feat(analytics): add Microsoft Clarity analytics
Add usage analytics via @microsoft/clarity across the frontend app and the login page. ClarityAnalytics loads the tracking script only in production builds (never local dev), keyed by NEXT_PUBLIC_CLARITY_ID; unset disables it. Mounts it in both the (frontend) root layout and the standalone login layout so both surfaces are covered without touching the admin panel.
This commit is contained in:
parent
64d92bf848
commit
4845084c1c
5 changed files with 45 additions and 1 deletions
3
bun.lock
3
bun.lock
|
|
@ -15,6 +15,7 @@
|
||||||
"@lexical/react": "0.41.0",
|
"@lexical/react": "0.41.0",
|
||||||
"@lexical/rich-text": "0.41.0",
|
"@lexical/rich-text": "0.41.0",
|
||||||
"@lexical/selection": "0.41.0",
|
"@lexical/selection": "0.41.0",
|
||||||
|
"@microsoft/clarity": "^1.0.2",
|
||||||
"@payloadcms/db-postgres": "3.88.0",
|
"@payloadcms/db-postgres": "3.88.0",
|
||||||
"@payloadcms/email-nodemailer": "3.88.0",
|
"@payloadcms/email-nodemailer": "3.88.0",
|
||||||
"@payloadcms/next": "3.88.0",
|
"@payloadcms/next": "3.88.0",
|
||||||
|
|
@ -541,6 +542,8 @@
|
||||||
|
|
||||||
"@lexical/yjs": ["@lexical/yjs@0.41.0", "", { "dependencies": { "@lexical/offset": "0.41.0", "@lexical/selection": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w=="],
|
"@lexical/yjs": ["@lexical/yjs@0.41.0", "", { "dependencies": { "@lexical/offset": "0.41.0", "@lexical/selection": "0.41.0", "lexical": "0.41.0" }, "peerDependencies": { "yjs": ">=13.5.22" } }, "sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w=="],
|
||||||
|
|
||||||
|
"@microsoft/clarity": ["@microsoft/clarity@1.0.2", "", {}, "sha512-9EZYROFpJxEGmQpHvUFqvD3ZJ7QQSqnibYSWmS+1xusoZfG1QQ1/Al9yVBBc11DWMbJrs1pe1hLT273it/skJg=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="],
|
||||||
|
|
||||||
"@monaco-editor/loader": ["@monaco-editor/loader@1.6.1", "", { "dependencies": { "state-local": "1.0.7" } }, "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg=="],
|
"@monaco-editor/loader": ["@monaco-editor/loader@1.6.1", "", { "dependencies": { "state-local": "1.0.7" } }, "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg=="],
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"@lexical/react": "0.41.0",
|
"@lexical/react": "0.41.0",
|
||||||
"@lexical/rich-text": "0.41.0",
|
"@lexical/rich-text": "0.41.0",
|
||||||
"@lexical/selection": "0.41.0",
|
"@lexical/selection": "0.41.0",
|
||||||
|
"@microsoft/clarity": "^1.0.2",
|
||||||
"@payloadcms/db-postgres": "3.88.0",
|
"@payloadcms/db-postgres": "3.88.0",
|
||||||
"@payloadcms/email-nodemailer": "3.88.0",
|
"@payloadcms/email-nodemailer": "3.88.0",
|
||||||
"@payloadcms/next": "3.88.0",
|
"@payloadcms/next": "3.88.0",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import { AnnouncementHost } from "@/components/frontend/announcements/Announceme
|
||||||
import type { AnnouncementEntry } from "@/lib/announcements/evaluate";
|
import type { AnnouncementEntry } from "@/lib/announcements/evaluate";
|
||||||
import { TourHost } from "@/components/frontend/tours/TourHost";
|
import { TourHost } from "@/components/frontend/tours/TourHost";
|
||||||
import { DevDashboardGate } from "@/components/frontend/dev/DevDashboardGate";
|
import { DevDashboardGate } from "@/components/frontend/dev/DevDashboardGate";
|
||||||
|
import { ClarityAnalytics } from "@/components/frontend/analytics/ClarityAnalytics";
|
||||||
|
|
||||||
// Skip static prerendering of this layout (and all pages under (frontend)/).
|
// Skip static prerendering of this layout (and all pages under (frontend)/).
|
||||||
// The layout calls `getPayload()` at render time to resolve the current user,
|
// The layout calls `getPayload()` at render time to resolve the current user,
|
||||||
|
|
@ -242,6 +243,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||||
{isLogistics && <ShipmentToasts />}
|
{isLogistics && <ShipmentToasts />}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ShimLoader shims={shimEntries} userId={user?.id ?? null} userRoles={user?.roles ?? []} />
|
<ShimLoader shims={shimEntries} userId={user?.id ?? null} userRoles={user?.roles ?? []} />
|
||||||
|
<ClarityAnalytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import "@/app/(frontend)/styles.css";
|
import "@/app/(frontend)/styles.css";
|
||||||
|
import { ClarityAnalytics } from "@/components/frontend/analytics/ClarityAnalytics";
|
||||||
|
|
||||||
export default function LoginLayout(props: { children: React.ReactNode }) {
|
export default function LoginLayout(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
|
<html lang="en" className="bg-black leading-8 antialiased h-full dark">
|
||||||
<body className="h-full font-sans m-0 text-white">{props.children}</body>
|
<body className="h-full font-sans m-0 text-white">
|
||||||
|
{props.children}
|
||||||
|
<ClarityAnalytics />
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/components/frontend/analytics/ClarityAnalytics.tsx
Normal file
34
src/components/frontend/analytics/ClarityAnalytics.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Clarity from "@microsoft/clarity";
|
||||||
|
|
||||||
|
// NEXT_PUBLIC_ vars are statically inlined at build time, so an unset
|
||||||
|
// NEXT_PUBLIC_CLARITY_ID is baked in as undefined for that build.
|
||||||
|
const CLARITY_ID = process.env.NEXT_PUBLIC_CLARITY_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft Clarity usage analytics (https://clarity.microsoft.com).
|
||||||
|
*
|
||||||
|
* Renders nothing. Initializes only when the build is a production build
|
||||||
|
* AND NEXT_PUBLIC_CLARITY_ID was set at build time, so local dev and
|
||||||
|
* ID-less builds stay untracked. Clarity records SPA route changes on its
|
||||||
|
* own, so no per-navigation wiring is needed.
|
||||||
|
*
|
||||||
|
* Mount once per root layout that should be tracked: the (frontend) layout
|
||||||
|
* and the standalone /login layout. The Payload admin panel is untracked.
|
||||||
|
*/
|
||||||
|
export function ClarityAnalytics() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.NODE_ENV !== "production" || !CLARITY_ID) return;
|
||||||
|
try {
|
||||||
|
Clarity.init(CLARITY_ID);
|
||||||
|
} catch (e) {
|
||||||
|
// Fire-and-forget: analytics must never break the app if the script
|
||||||
|
// is blocked (ad blockers) or Clarity is unreachable.
|
||||||
|
console.warn("[clarity] init failed:", e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue