ADR 0112 — Clarity install method: manual script vs NPM package
Status: Accepted
Date: 2026-06-04 UTC
Deciders: Kristerpher (operator)
Scope: getraxx.com (Vite static, frontend/getraxx-landing/), raxx.app (Next.js 15 App Router + CF Pages, frontend/raxx-next/)
Context
Clarity project wsw9s57qh9 is provisioned. Two surfaces need wiring:
- getraxx.com already has a consent gate (
analyticsGate.js) that callswindow.clarity('consent')after accept. It expectswindow.clarityto already exist — meaning the script loader must run before consent fires, but recording must not start until consent fires. The missing piece is the loader injection +window.__RAXX_CLARITY_ID__assignment. - raxx.app (raxx-next) has zero Clarity wiring. The CRA tracker in
trademaster_ui/is deleted by #2968 on 2026-06-11. A fresh implementation is required. An in-flight feature-developer agent has already written a manual-script port (lib/clarityTracker.ts+components/analytics/ClarityProvider.tsx).
Microsoft Clarity offers two install paths: (B) copy/paste the IIFE loader into the page, or (C) @microsoft/clarity npm package. The choice affects bundle, testability, typing, edge-runtime safety, and alignment with the existing in-flight work.
Options
| Criterion | B — Manual IIFE snippet | C — NPM @microsoft/clarity |
|---|---|---|
| Bundle size | Zero — script loaded async from CDN; no bytes added to app bundle | 7.6 KB unpacked; index.js + utils.js; no transitive deps; negligible after tree-shake / gzip (~1 KB) |
| Type safety | Requires manual window & { clarity?: fn } casts (already in place in the existing TS port) |
Ships index.d.ts with typed .init(), .setTag(), .consent(), .identify(), etc. |
| Test mockability | window.clarity stub + _resetForTest() helper — already proven in CRA tests and ported to raxx-next Vitest suite |
Same — init() calls window only inside injectScript(); safe to mock window.clarity identically |
| Framework idioms | Bare IIFE inline in TS — requires eslint-disable blocks + manual async-queue pattern already present in CDN snippet |
Clarity.init(id) call inside useEffect — idiomatic, readable, no lint suppression needed |
| Upgrade path | Snippet pinned to the CDN version at time of copy; Clarity backend changes that alter the loader must be manually tracked | npm update @microsoft/clarity propagates loader changes; version diff in lockfile |
| Consent-gating ergonomics | Custom: inject snippet on page load (buffers calls), fire window.clarity('consent') after accept — works; getraxx already implements this split |
Clarity.init(id) deferred until consent; Clarity.consent() method explicit — equally ergonomic |
| SSR / edge runtime safety | Safe — IIFE only referenced inside useEffect ('use client'); no import-time window access |
Safe — injectScript() touches window + document only when called, NOT at import time (verified by source inspection) |
| Excluded-path support | Handled by caller (initClarity()): check pathname before calling IIFE — already implemented |
Identical: check pathname before calling Clarity.init() — no difference |
| Founder/investor tag setting | window.clarity('set', k, v) — already implemented in setFounderTags() |
Clarity.setTag(k, v) — typed wrapper around same call |
Key finding from package inspection: @microsoft/clarity@1.0.2 (the current release) does NOT access window or document at import time. The window calls are inside injectScript() which is only invoked from Clarity.init(). The package is therefore fully edge-runtime safe.
Decision
Manual script on both surfaces — continue in-flight Option B implementation unchanged.
Rationale:
-
The in-flight feature-developer (
a4c24b49104583fc8) has already produced a correct, typed, tested manual implementation (lib/clarityTracker.ts,ClarityProvider.tsx) that handles flag-gating, path exclusion, consent split, founder tags, and test isolation. It is functionally complete. Redirecting to NPM would require tearing out a working implementation to gain minimal marginal benefit. -
The NPM package (
@microsoft/clarity) is a thin wrapper — itsinjectScript()body is character-for-character identical to the manual IIFE already in the implementation. The type definitions add value for new code but the types are already replicated via interface declarations inclarityTracker.ts. The net gain does not justify a redirect. -
getraxx.com is a Vite static build with a plain-JS consent gate (
analyticsGate.js) that is framework-agnostic by design. Introducing an npm module here for a three-line init gains nothing and adds a dependency to a build that currently has zero Clarity npm footprint. -
The NPM package releases independently of Clarity's CDN. If Microsoft updates the CDN loader script, the npm package may lag or diverge. The manual approach tracks the CDN directly.
-
One open question remains (see below): the NPM package emits
?ref=npmon its CDN tag URL, which deducts a CDN-load hit from npm installs vs direct embeds. This is cosmetic but worth noting if Clarity session attribution data is compared between surfaces.
Language choice rationale
N/A — this ADR governs a client-side analytics install method, not a new service.
Consequences
Positive
- In-flight feature-developer work is unblocked and continues without redirection.
- No new npm dependency added to either surface.
- Existing test patterns (
window.claritystub +_resetForTest()) are reused verbatim across CRA → raxx-next port. - Manual snippet matches CDN loader exactly; no version-skew risk between npm wrapper and CDN.
Negative / risks
- No typed
Clarity.*API surface in the app; callers must use untypedwindow.clarity(...)for any call not wrapped inclarityTracker.ts. Mitigated: the tracker wraps all calls we need. - Snippet upgrades are manual; if Microsoft changes the loader IIFE, a PR is required to update it.
Neutral
- The getraxx consent flow (load snippet on page load, fire
clarity('consent')after accept) differs from raxx-next (defer init until after FLAG + consent). Both are correct; they reflect the different consent architectures on the two surfaces.
Alternatives considered
C — NPM @microsoft/clarity on raxx-next only (hybrid)
Rejected. The NPM package's body is functionally identical to the manual IIFE already implemented. Introducing it mid-implementation to gain typed method names on an already-typed wrapper adds churn with no runtime benefit.
C — NPM on both surfaces
Rejected. getraxx.com is plain JS; adding an npm module for an init call that is five lines long is not worth the dependency.
Security / GDPR checklist
- PII collected: Session replay captures DOM state. Clarity-side "Mask All" is the project default; must be verified in staging replay before FLAG_CLARITY_ENABLED is turned on for real users.
- Retention period: Controlled by Clarity project settings at clarity.microsoft.com (default 30 days). Not configurable in code.
- Deletion on DSR: Clarity provides a project-level data deletion API. DSR flow must include a Clarity deletion call — this is a gap; see open question below.
- Audit trail: FLAG_CLARITY_ENABLED state changes are subject to the platform flag audit trail. Clarity itself does not emit to the Raptor audit log.
- Stored credentials: None. Project ID
wsw9s57qh9is public-safe per Clarity docs. - Breach notification path: Clarity is a Microsoft-operated service; breach notification is governed by the Microsoft DPA. Operator must confirm DPA is in place.
- Secrets location + rotation: No secrets. Project ID is a public identifier embedded in the build.
- Kill-switch:
FLAG_CLARITY_ENABLED = 0stops all Clarity activity immediately without redeploy. Path exclusion for/privacy/data-requestis hardcoded as a secondary invariant.
Open questions
- DSR gap: No automation exists to call the Clarity data-deletion API on a data subject request. This must be addressed before FLAG_CLARITY_ENABLED is turned on for real users. Needs an implementation sub-card.
- Microsoft DPA: Has the operator signed or accepted the Microsoft Clarity Data Processing Addendum? Required before recording real customer sessions under GDPR/CCPA.
- Staging project ID: The current default project ID (
wsw9s57qh9) is the production project. A separate staging project ID should be provisioned to prevent staging sessions from polluting prod replay data. Needs an operator-action card. - getraxx loader injection:
analyticsGate.jsexpectswindow.clarityto already exist whenflush()is called. The missing piece (index.html snippet injection +window.__RAXX_CLARITY_ID__assignment) is in the feature-developer's scope — confirm it is included in the in-flight PR.
Revisit when
@microsoft/clarityships a major version with material new API surface (e.g. server-side recording, first-party proxy support) — at that point the NPM wrapper provides more than the manual IIFE.- raxx-next migrates off CF Pages to a Node.js host — SSR/edge concerns evaporate and
next/scriptStrategy="afterInteractive" becomes a simpler install path.