GA4 and Microsoft Clarity run only on the website, after explicit analytics
acceptance. They are not part of the desktop app. Source:
src/lib/analytics.ts, src/lib/analyticsConsent.ts, and
src/components/PrivacyChoices.tsx. Policy text lives in src/data/legal.ts.
The same choice panel is prerendered into #openstudio-privacy, outside the
temporarily hidden route root. A small pre-paint script uses the shared
consentRecord.ts validator to hide it for an unexpired saved choice. This
script never loads analytics or grants consent. The client portal replaces the
inert static panel before paint and keeps router links, focus and choice handlers
inside the existing app. Without JavaScript, or when initial application loading
fails, the static panel stays hidden so unusable controls cannot cover legal text.
The original loader still covers initial non-legal navigation until it finishes.
Set these in the hosting provider's build environment; Vite embeds them at build time. Changing a value requires a new build.
| Variable | Meaning |
|---|---|
VITE_GA_MEASUREMENT_ID |
Public GA4 web-stream measurement ID (G-…). Omit to disable Google Analytics. |
VITE_CLARITY_PROJECT_ID |
Public Clarity project ID. Omit to disable Clarity. |
VITE_ANALYTICS_ENABLED |
Usually leave unset: only production builds on openstudio.org.in and www.openstudio.org.in enable analytics. false disables both everywhere; true enables configured providers on any host for controlled verification. Consent remains required. |
These IDs are public configuration, not API secrets. Do not put credentials in
any VITE_ variable. Keep VITE_ANALYTICS_ENABLED unset for deploy previews and
ordinary local development so those visits do not enter production reports.
The React router reports a view once the page and its metadata are ready. Keep this method for accurate titles on lazy routes. Do not add a second GA snippet in HTML, GTM or a hosting plugin.
In the matching Google Analytics web stream:
- Open Admin → Data collection and modification → Data streams.
- Choose the stream matching
VITE_GA_MEASUREMENT_ID. - Open the Enhanced measurement settings (gear).
- Expand Page views → Show advanced settings.
- Turn Page changes based on browser history events off, then save.
Other Enhanced Measurement options can remain enabled. The application already
sets send_page_view: false to suppress the initial view from the config
command. That flag does not disable automatic history views; the account
setting above is also required. Do not replace this with an undocumented tag
option or intercept Google's requests in production.
See Google's page-view documentation and SPA measurement.
page_view: one manual event per rendered pathname, including back/forward navigation. Query/hash-only changes or duplicate readiness callbacks do not create additional views. Reacceptance starts a new consented page context.page_engagement_time: custom elapsed visible-page duration withduration_ms,duration_seconds,duration_bucket,exit_reason, and maximum scroll depth. It carries the previous page URL/title/referrer when leaving a route. It does not override Google's built-inengagement_time_msec.scroll_depth_reached: once each at 25/50/75/90% in the current visible-page session. Google's optional automaticscrollis a separate event.file_download_clicked,outbound_link_clicked,mailto_link_clicked: delegated link-click tracking. Installer events contain the actual GitHub artifact URL. They measure click intent, not download completion or installs. Don't sumfile_download_clickedwith Google's automaticfile_downloadwhen reporting downloads. Same-origin redirect URLs without file extensions are not detected as file-download clicks by the generic extension matcher.
Manual page views and events capture URL/title/referrer when they occur, so a
delayed provider load cannot move them to the page active when the queue drains.
The first referrer comes from document.referrer; subsequent manual views use
the last consented route. URLs in this manual context exclude query and hash.
This is not a blanket redaction guarantee for Clarity or provider-generated
events: configure provider URL masking/redaction separately if required. Because
manual page URLs omit query strings, do not assume UTM campaign attribution is
preserved by this implementation.
Clarity receives these custom event names, while GA receives their parameters.
Clarity records interactions using its own runtime; the site does not manually
create Clarity page views. Its consent calls use
Microsoft's consentv2 API.
- Neither provider loads before acceptance. Pre-consent activity is discarded.
- Accepted providers load after the initial content/intro readiness gate, then idle scheduling or the next input. Very short visits may end before providers initialize; this is a deliberate loading-performance tradeoff.
- Rejection cancels deferred initialization, discards queued site events, disables GA, denies storage consent, stops Clarity and clears accessible first-party analytics cookies. Already accepted/buffered activity can still finish uploading; third-party-domain cookies cannot be cleared by this site.
- Reacceptance resumes providers without a reload or duplicate script tags.
- Decisions last up to 180 days. Expiry, storage clearing, cross-tab changes and unavailable storage are covered by the consent tests. Advertising consent remains denied and Google advertising personalization/signals stay disabled.
node --test test/analytics.test.mjs test/analytics-consent-browser.test.mjs test/analytics-verifier.test.mjs
npm run verify:analytics -- --url https://openstudio.org.in
npm run verify:analytics -- --url https://openstudio.org.in --mobileThe automated suite uses provider stand-ins for repeatable consent and page-context regressions. It cannot verify the GA account's history setting.
verify:analytics loads the real Google and Clarity scripts in a fresh Chromium
context. It intercepts provider uploads before they leave the browser and prevents
installer navigation, so it does not add test traffic to reports or download
binaries. It checks initial consent, actual collection requests, exactly one view
per client navigation (including Back), referrers, engagement page attribution,
and all three installer click events. It fails if analytics are disabled, provider
scripts are blocked, or duplicate/missing events occur. Allow about a minute.
Run this on an analytics-enabled deployment after changes to routing, consent or provider settings. The live check is separate from offline CI; it depends on external provider scripts and account configuration. The decoder/assertion tests also demonstrate that duplicate history events fail the live check.
Account owners should additionally confirm GA Realtime/DebugView and Clarity recordings, retention, masking, and desired GA key-event/custom-dimension settings. The interception check proves client behavior, not dashboard ingestion.
The audit found two /docs views for one navigation and a custom engagement event
that mixed /privacy metadata with /docs as its URL. Code now preserves page
context and regression coverage includes the mismatch. Disabling history page
views is a separate GA account change; a code deployment alone does not complete
that correction. The real-provider verifier must pass before marking it resolved.
The first production run of npm run verify:analytics -- --url https://openstudio.org.in
failed the exactly-one-page-view check: the initial /privacy view was
followed by two views for each /docs, /download and Back-to-/docs navigation.
The OpenStudio Website stream's measurement ID was matched against the production
JavaScript bundle. Page changes based on browser history events was enabled;
it was turned off, saved and confirmed off in the reopened settings. Other
Enhanced Measurement options remain enabled. This account correction takes effect
without deploying application code.
Fresh desktop and mobile runs of the commands above then passed: each produced
exactly four page views (/privacy, /docs, /download, Back-to-/docs) and three
installer click events, with correct referrers and engagement attribution. Both
confirmed no provider requests before consent and real Clarity upload generation.
Provider uploads were intercepted, so these checks did not add synthetic traffic
to reports. The duplicate history page-view issue is resolved for these live
client checks; dashboard ingestion is outside their scope.
Evidence is in ignored output/review/pr24-merge-readiness/analytics-live.log
(before), analytics-fixed-desktop.log and analytics-fixed-mobile.log (after).