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",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/mdx": "^2.0.1",
|
||||
"@types/mdx-js__react": "^1.5.5",
|
||||
|
@ -71,6 +72,7 @@
|
|||
"csstype": "^3.0.11",
|
||||
"date-fns": "^2.29.3",
|
||||
"dayjs": "^1.10.5",
|
||||
"debug": "^4.3.4",
|
||||
"deep-object-diff": "^1.1.9",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dnd-core": "^16.0.0",
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
type RedditReportType,
|
||||
reportToGoogle,
|
||||
reportToReddit,
|
||||
plausibleDataDomain,
|
||||
} from './utils';
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -88,6 +103,7 @@ export function GlobalScripts() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<PlausibleScripts />
|
||||
<GoogleScripts userEmailHash={userEmailHash} />
|
||||
<RedditScripts userEmailHash={userEmailHash} />
|
||||
</>
|
||||
|
|
|
@ -13,6 +13,7 @@ export enum GtagConversionId {
|
|||
}
|
||||
|
||||
export const redditPixelId = 't2_ggt11omdo';
|
||||
export const plausibleDataDomain = 'cloud.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 { useConsoleRoutes } from '@/hooks/use-console-routes';
|
||||
import { usePlausiblePageview } from '@/hooks/use-plausible-pageview';
|
||||
|
||||
import type { AppContentOutletContext } from '../AppContent/types';
|
||||
|
||||
|
@ -11,9 +12,11 @@ import * as styles from './index.module.scss';
|
|||
|
||||
function ConsoleContent() {
|
||||
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.
|
||||
useTenantScopeListener();
|
||||
usePlausiblePageview(routeObjects);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { condArray } from '@silverhand/essentials';
|
||||
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 Dashboard from '@/pages/Dashboard';
|
||||
|
@ -54,7 +54,6 @@ export const useConsoleRoutes = () => {
|
|||
),
|
||||
[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;
|
||||
// Reddit
|
||||
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 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';
|
||||
|
||||
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() {
|
||||
const routes = useRoutes(routeObjects);
|
||||
|
||||
usePlausiblePageview(routeObjects);
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<Topbar />
|
||||
<div className={styles.content}>
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className={styles.content}>{routes}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,13 +15,10 @@ import Toast from '@/ds-components/Toast';
|
|||
import useCurrentUser from '@/hooks/use-current-user';
|
||||
import useSwrOptions from '@/hooks/use-swr-options';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
import AppContent from './containers/AppContent';
|
||||
import useUserOnboardingData from './hooks/use-user-onboarding-data';
|
||||
import * as styles from './index.module.scss';
|
||||
import SignInExperience from './pages/SignInExperience';
|
||||
import Welcome from './pages/Welcome';
|
||||
import { OnboardingPage, OnboardingRoute } from './types';
|
||||
import { getOnboardingPage } from './utils';
|
||||
|
||||
|
@ -88,12 +85,7 @@ export function OnboardingRoutes() {
|
|||
<Route element={<TenantAccess />}>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Navigate replace to={OnboardingRoute.Onboarding} />} />
|
||||
<Route path={OnboardingRoute.Onboarding} 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 path="*" element={<AppContent />} />
|
||||
</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':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/debug':
|
||||
specifier: ^4.1.7
|
||||
version: 4.1.7
|
||||
'@types/jest':
|
||||
specifier: ^29.4.0
|
||||
version: 29.4.0
|
||||
|
@ -2843,6 +2846,9 @@ importers:
|
|||
dayjs:
|
||||
specifier: ^1.10.5
|
||||
version: 1.11.6
|
||||
debug:
|
||||
specifier: ^4.3.4
|
||||
version: 4.3.4
|
||||
deep-object-diff:
|
||||
specifier: ^1.1.9
|
||||
version: 1.1.9
|
||||
|
@ -15656,7 +15662,7 @@ packages:
|
|||
jest: ^28.1.0 || ^29.1.2
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
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
|
||||
dev: true
|
||||
|
||||
|
|
Loading…
Reference in a new issue