mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): add plausible
This commit is contained in:
parent
d545303568
commit
a2bbc250ca
11 changed files with 128 additions and 19 deletions
|
@ -54,6 +54,7 @@
|
||||||
"@swc/jest": "^0.2.26",
|
"@swc/jest": "^0.2.26",
|
||||||
"@testing-library/react": "^15.0.0",
|
"@testing-library/react": "^15.0.0",
|
||||||
"@types/color": "^3.0.3",
|
"@types/color": "^3.0.3",
|
||||||
|
"@types/debug": "^4.1.7",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/mdx": "^2.0.1",
|
"@types/mdx": "^2.0.1",
|
||||||
"@types/mdx-js__react": "^1.5.5",
|
"@types/mdx-js__react": "^1.5.5",
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
"csstype": "^3.0.11",
|
"csstype": "^3.0.11",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"dayjs": "^1.10.5",
|
"dayjs": "^1.10.5",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"deep-object-diff": "^1.1.9",
|
"deep-object-diff": "^1.1.9",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"dnd-core": "^16.0.0",
|
"dnd-core": "^16.0.0",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
type RedditReportType,
|
type RedditReportType,
|
||||||
reportToGoogle,
|
reportToGoogle,
|
||||||
reportToReddit,
|
reportToReddit,
|
||||||
|
plausibleDataDomain,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
type ScriptProps = {
|
type ScriptProps = {
|
||||||
|
@ -61,6 +62,20 @@ function RedditScripts({ userEmailHash }: ScriptProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlausibleScripts() {
|
||||||
|
return (
|
||||||
|
<Helmet>
|
||||||
|
<script
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
data-domain={plausibleDataDomain}
|
||||||
|
src="https://plausible.io/js/plausible.manual.js"
|
||||||
|
/>
|
||||||
|
<script>{`window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }`}</script>
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders global scripts for conversion tracking.
|
* Renders global scripts for conversion tracking.
|
||||||
*/
|
*/
|
||||||
|
@ -88,6 +103,7 @@ export function GlobalScripts() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PlausibleScripts />
|
||||||
<GoogleScripts userEmailHash={userEmailHash} />
|
<GoogleScripts userEmailHash={userEmailHash} />
|
||||||
<RedditScripts userEmailHash={userEmailHash} />
|
<RedditScripts userEmailHash={userEmailHash} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -13,6 +13,7 @@ export enum GtagConversionId {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const redditPixelId = 't2_ggt11omdo';
|
export const redditPixelId = 't2_ggt11omdo';
|
||||||
|
export const plausibleDataDomain = 'cloud.logto.io';
|
||||||
|
|
||||||
const logtoProductionHostname = 'logto.io';
|
const logtoProductionHostname = 'logto.io';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext, useRoutes } from 'react-router-dom';
|
||||||
|
|
||||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||||
import { useConsoleRoutes } from '@/hooks/use-console-routes';
|
import { useConsoleRoutes } from '@/hooks/use-console-routes';
|
||||||
|
import { usePlausiblePageview } from '@/hooks/use-plausible-pageview';
|
||||||
|
|
||||||
import type { AppContentOutletContext } from '../AppContent/types';
|
import type { AppContentOutletContext } from '../AppContent/types';
|
||||||
|
|
||||||
|
@ -11,9 +12,11 @@ import * as styles from './index.module.scss';
|
||||||
|
|
||||||
function ConsoleContent() {
|
function ConsoleContent() {
|
||||||
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
|
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
|
||||||
const routes = useConsoleRoutes();
|
const routeObjects = useConsoleRoutes();
|
||||||
|
const routes = useRoutes(routeObjects);
|
||||||
// Use this hook here to make sure console listens to user tenant scope changes.
|
// Use this hook here to make sure console listens to user tenant scope changes.
|
||||||
useTenantScopeListener();
|
useTenantScopeListener();
|
||||||
|
usePlausiblePageview(routeObjects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { condArray } from '@silverhand/essentials';
|
import { condArray } from '@silverhand/essentials';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { type RouteObject, useRoutes } from 'react-router-dom';
|
import { type RouteObject } from 'react-router-dom';
|
||||||
|
|
||||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
|
@ -54,7 +54,6 @@ export const useConsoleRoutes = () => {
|
||||||
),
|
),
|
||||||
[tenantSettings]
|
[tenantSettings]
|
||||||
);
|
);
|
||||||
const routes = useRoutes(routeObjects);
|
|
||||||
|
|
||||||
return routes;
|
return routeObjects;
|
||||||
};
|
};
|
||||||
|
|
26
packages/console/src/hooks/use-plausible-pageview.ts
Normal file
26
packages/console/src/hooks/use-plausible-pageview.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { appendPath } from '@silverhand/essentials';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { type RouteObject, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { plausibleDataDomain } from '@/components/Conversion/utils';
|
||||||
|
import { getRoutePattern } from '@/utils/route';
|
||||||
|
|
||||||
|
const log = debug('usePlausiblePageview');
|
||||||
|
|
||||||
|
export const usePlausiblePageview = (routes: RouteObject[]) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const routePattern = getRoutePattern(pathname, routes);
|
||||||
|
|
||||||
|
log('pageview', routePattern);
|
||||||
|
|
||||||
|
// https://plausible.io/docs/custom-locations#3-specify-a-custom-location
|
||||||
|
window.plausible?.('pageview', {
|
||||||
|
u:
|
||||||
|
appendPath(new URL('https://' + plausibleDataDomain), routePattern).href +
|
||||||
|
window.location.search,
|
||||||
|
});
|
||||||
|
}, [pathname, routes]);
|
||||||
|
};
|
2
packages/console/src/include.d/tags.d.ts
vendored
2
packages/console/src/include.d/tags.d.ts
vendored
|
@ -3,4 +3,6 @@ declare interface Window {
|
||||||
gtag?: (...args: unknown[]) => void;
|
gtag?: (...args: unknown[]) => void;
|
||||||
// Reddit
|
// Reddit
|
||||||
rdt?: (...args: unknown[]) => void;
|
rdt?: (...args: unknown[]) => void;
|
||||||
|
// Plausible
|
||||||
|
plausible?: (...args: unknown[]) => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,47 @@
|
||||||
import { Outlet } from 'react-router-dom';
|
import { useRoutes, type RouteObject, Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { usePlausiblePageview } from '@/hooks/use-plausible-pageview';
|
||||||
import Topbar from '@/onboarding/components/Topbar';
|
import Topbar from '@/onboarding/components/Topbar';
|
||||||
|
import SignInExperience from '@/onboarding/pages/SignInExperience';
|
||||||
|
import Welcome from '@/onboarding/pages/Welcome';
|
||||||
|
import { OnboardingPage, OnboardingRoute } from '@/onboarding/types';
|
||||||
|
import NotFound from '@/pages/NotFound';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
const routeObjects: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: OnboardingRoute.Onboarding,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Navigate replace to={OnboardingPage.Welcome} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: OnboardingPage.Welcome,
|
||||||
|
element: <Welcome />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: OnboardingPage.SignInExperience,
|
||||||
|
element: <SignInExperience />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <NotFound />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
|
const routes = useRoutes(routeObjects);
|
||||||
|
|
||||||
|
usePlausiblePageview(routeObjects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<Topbar />
|
<Topbar />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>{routes}</div>
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,10 @@ import Toast from '@/ds-components/Toast';
|
||||||
import useCurrentUser from '@/hooks/use-current-user';
|
import useCurrentUser from '@/hooks/use-current-user';
|
||||||
import useSwrOptions from '@/hooks/use-swr-options';
|
import useSwrOptions from '@/hooks/use-swr-options';
|
||||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||||
import NotFound from '@/pages/NotFound';
|
|
||||||
|
|
||||||
import AppContent from './containers/AppContent';
|
import AppContent from './containers/AppContent';
|
||||||
import useUserOnboardingData from './hooks/use-user-onboarding-data';
|
import useUserOnboardingData from './hooks/use-user-onboarding-data';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import SignInExperience from './pages/SignInExperience';
|
|
||||||
import Welcome from './pages/Welcome';
|
|
||||||
import { OnboardingPage, OnboardingRoute } from './types';
|
import { OnboardingPage, OnboardingRoute } from './types';
|
||||||
import { getOnboardingPage } from './utils';
|
import { getOnboardingPage } from './utils';
|
||||||
|
|
||||||
|
@ -88,12 +85,7 @@ export function OnboardingRoutes() {
|
||||||
<Route element={<TenantAccess />}>
|
<Route element={<TenantAccess />}>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route index element={<Navigate replace to={OnboardingRoute.Onboarding} />} />
|
<Route index element={<Navigate replace to={OnboardingRoute.Onboarding} />} />
|
||||||
<Route path={OnboardingRoute.Onboarding} element={<AppContent />}>
|
<Route path="*" element={<AppContent />} />
|
||||||
<Route index element={<Navigate replace to={OnboardingPage.Welcome} />} />
|
|
||||||
<Route path={OnboardingPage.Welcome} element={<Welcome />} />
|
|
||||||
<Route path={OnboardingPage.SignInExperience} element={<SignInExperience />} />
|
|
||||||
</Route>
|
|
||||||
<Route path="*" element={<NotFound />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
31
packages/console/src/utils/route.ts
Normal file
31
packages/console/src/utils/route.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { matchRoutes, type RouteObject } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const getRoutePattern = (pathname: string, routes: RouteObject[]) => {
|
||||||
|
// Remove the first segment of the pathname, which is the tenant ID.
|
||||||
|
const normalized = pathname.replace(/^\/[^/]+/, '');
|
||||||
|
const matches = matchRoutes(routes, normalized) ?? [];
|
||||||
|
return (
|
||||||
|
'/' +
|
||||||
|
matches
|
||||||
|
.filter((match) => !match.route.index)
|
||||||
|
.flatMap(({ route: { path }, params }) => {
|
||||||
|
// Path could have multiple segments, e.g. 'api-resources/:id/*'.
|
||||||
|
const segments = path?.split('/') ?? [];
|
||||||
|
|
||||||
|
return segments.map((segment) => {
|
||||||
|
if (segment === '*') {
|
||||||
|
return params['*'] ?? segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is not a parameter, or it's an ID parameter, use the path as is.
|
||||||
|
if (!segment.startsWith(':') || segment.endsWith('Id') || segment.endsWith('id')) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the parameter value.
|
||||||
|
return params[segment.slice(1)] ?? segment;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.join('/')
|
||||||
|
);
|
||||||
|
};
|
|
@ -2792,6 +2792,9 @@ importers:
|
||||||
'@types/color':
|
'@types/color':
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
'@types/debug':
|
||||||
|
specifier: ^4.1.7
|
||||||
|
version: 4.1.7
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: ^29.4.0
|
specifier: ^29.4.0
|
||||||
version: 29.4.0
|
version: 29.4.0
|
||||||
|
@ -2843,6 +2846,9 @@ importers:
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.10.5
|
specifier: ^1.10.5
|
||||||
version: 1.11.6
|
version: 1.11.6
|
||||||
|
debug:
|
||||||
|
specifier: ^4.3.4
|
||||||
|
version: 4.3.4
|
||||||
deep-object-diff:
|
deep-object-diff:
|
||||||
specifier: ^1.1.9
|
specifier: ^1.1.9
|
||||||
version: 1.1.9
|
version: 1.1.9
|
||||||
|
@ -15656,7 +15662,7 @@ packages:
|
||||||
jest: ^28.1.0 || ^29.1.2
|
jest: ^28.1.0 || ^29.1.2
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
jest: 29.7.0(@types/node@20.11.20)
|
jest: 29.7.0(@types/node@20.11.20)(ts-node@10.9.2)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue