Raxx · internal docs

internal · gated

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:

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 SafeinjectScript() 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:

  1. 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.

  2. The NPM package (@microsoft/clarity) is a thin wrapper — its injectScript() 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 in clarityTracker.ts. The net gain does not justify a redirect.

  3. 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.

  4. 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.

  5. One open question remains (see below): the NPM package emits ?ref=npm on 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

Negative / risks

Neutral


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


Open questions

  1. 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.
  2. Microsoft DPA: Has the operator signed or accepted the Microsoft Clarity Data Processing Addendum? Required before recording real customer sessions under GDPR/CCPA.
  3. 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.
  4. getraxx loader injection: analyticsGate.js expects window.clarity to already exist when flush() 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