1
0
Fork 0

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:
Jason Fraley 2026-10-01 10:04:45 -04:00
parent 64d92bf848
commit 4845084c1c
5 changed files with 45 additions and 1 deletions

View file

@ -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=="],

View file

@ -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",

View file

@ -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>
); );

View file

@ -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>
); );
} }

View 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;
}