diff --git a/apps/web/package.json b/apps/web/package.json index 7b7b1ab4..515a02ed 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3", + "@microsoft/clarity": "^1.0.2", "@monaco-editor/react": "^4.7.0", "@peculiar/x509": "^2.0.0", "@radix-ui/react-accordion": "^1.2.12", diff --git a/apps/web/src/components/clarity-analytics.tsx b/apps/web/src/components/clarity-analytics.tsx new file mode 100644 index 00000000..f223230e --- /dev/null +++ b/apps/web/src/components/clarity-analytics.tsx @@ -0,0 +1,20 @@ +"use client" + +import { useEffect } from "react" + +// Microsoft Clarity heatmaps + session replays. No-op unless +// NEXT_PUBLIC_CLARITY_PROJECT_ID is set (so it stays off in local dev), +// matching how Vercel Analytics only reports in production. +// Data lives in clarity.microsoft.com — nothing touches our backend. +let initialized = false // ponytail: module guard so StrictMode/remounts can't double-inject the script + +export function ClarityAnalytics() { + useEffect(() => { + const projectId = process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID + if (!projectId || initialized) return + initialized = true + import("@microsoft/clarity").then((m) => m.default.init(projectId)) + }, []) + + return null +} diff --git a/apps/web/src/components/client-shell.tsx b/apps/web/src/components/client-shell.tsx index f24ba9fb..0a0e9b02 100644 --- a/apps/web/src/components/client-shell.tsx +++ b/apps/web/src/components/client-shell.tsx @@ -20,6 +20,10 @@ const ClientSpeedInsights = dynamic( () => import('@vercel/speed-insights/next').then((m) => m.SpeedInsights), { ssr: false } ) +const ClientClarity = dynamic( + () => import('@/components/clarity-analytics').then((m) => m.ClarityAnalytics), + { ssr: false } +) const ClientToaster = dynamic( () => import('@/components/branded-toaster').then((m) => m.BrandedToaster), { ssr: false } @@ -47,6 +51,9 @@ export function ClientShell({ children }: Props) { + + +