Core: provider-agnostic analytics plugin with Statsig adapter (full legacy parity) - #74384
Open
stephenliang wants to merge 3 commits into
Open
Core: provider-agnostic analytics plugin with Statsig adapter (full legacy parity)#74384stephenliang wants to merge 3 commits into
stephenliang wants to merge 3 commits into
Conversation
stephenliang
marked this pull request as ready for review
August 4, 2026 22:55
Add @code-dot-org/core/plugins/analytics so frontend/ packages can log product analytics without importing the legacy apps/ bundle. The plugin follows the observability plugin's shape: a module-level singleton (sendEvent/setUser) backed by a no-op adapter until boot, a deferred adapter buffering calls during async provider load, and a factory that lazy-imports the Statsig adapter so @statsig/js-client stays out of bundles that do not opt in. Behavior is full parity with apps/src/metrics/StatsigReporter.js so the data flowing to Statsig does not change: - Consent never gates event sending. It gates only stable-ID persistence, decided once per page load at OneTrust's first report, exactly like the legacy oneTrustPromise flow. A C0002 grant persists the legacy cookie/localStorage ID; anything else deletes our copies and lets the SDK mint its own. - A page with OneTrust that never reports sends nothing (legacy's unresolved-promise behavior); a page without OneTrust sends immediately. The new consent/settled.ts leaf carries this signal; connectOneTrust drives it. - The consent store becomes tri-state (explicit true/false per category, absent = no CMP report) so an all-deny report is observable; the old Set shape deduped it away. - Init user carries custom.enabledExperiments and geRegion; identity updates replicate legacy setUserProperties byte-for-byte, including its omission of customIDs. logEvent(name, name, payload) parity. - Statsig config travels in the app-config meta; the transmit decision (production or managed-test) moves server-side into observability_config, replacing client-side local_mode. Development gets a console-logging adapter matching legacy's local mode. The users package fires the legacy 'Account Settings Page Visited' event at mount; the other five legacy account events belong to features whose UI does not exist in the new page yet. Studio registers the plugin and re-asserts identity from the router's beforeLoad. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stephenliang
force-pushed
the
stephen/core-analytics-plugin
branch
from
August 4, 2026 22:59
9284fb8 to
873ac21
Compare
🖼️ Storybook Visual Comparison Report✅ No Storybook eyes differences detected! |
- Statsig config moves out of the observability engine: analytics_config lives in StatsigAnalyticsHelper, the engine exposes an observability-only hash variant, and the frontend_studio view composes the app-config meta with a plain hash merge. Helper tests move to dashboard/test/helpers accordingly; lib/cdo/statsig.rb reverts. - The users package no longer declares @statsig/js-client; it externalizes core, so the provider chunk is never resolved at its build time. Studio keeps the declaration. - Docs and comments describe the current contract only, and comments narrating mechanics are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AppConfigHelper#app_config composes the meta JSON from the helpers that own each section; the observability engine helper returns to its original single-section form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@code-dot-org/core/plugins/analyticsso Next Generation Frontend Platform packages can log product analytics without importing the legacyapps/bundle (closes #73791). The public surface is deliberately small and provider-blind —sendEvent(name, payload),setUser(user), andanalyticsPluginforinitializeCore— with Statsig as the first provider, loaded as a lazy chunk so@statsig/js-clientstays out of bundles that don't opt in. The governing constraint throughout: full parity withapps/src/metrics/StatsigReporter.js, so the data flowing to Statsig does not change.How it works
sequenceDiagram participant Page as Host page head participant OT as OneTrust stub and SDK participant A as analyticsPlugin participant S as StatsigClient lazy chunk Page->>OT: load stub, define oneTrustPromise Page->>A: initializeCore -> onCoreReady A->>A: read provider from app-config meta A->>A: install DeferredAdapter, buffer sendEvent and setUser OT-->>A: first report -> consent state + settled signal Note over A: no OneTrust on page: settles immediately.<br/>OneTrust present but never loads: never settles,<br/>nothing sends (legacy unresolved-promise parity) A->>A: single consent decision per page load:<br/>C0002 grant -> persist stable ID,<br/>else delete our copies, SDK mints its own A->>S: dynamic import, init with config and session A->>S: flush buffered calls in orderConsent model (parity, not policy change)
Consent never gates event sending — it gates only stable-ID persistence, decided once per page load at OneTrust's first report, exactly mirroring legacy's
oneTrustPromise.then(initializeAfterConsent). A C0002 grant persists the legacy cookie (statsig_stable_idon.code.org) and localStorage key byte-for-byte, so IDs carry across legacy pages, new pages, and the Rails server-side reader. Anything else deletes our persisted copies and passesstableID: undefinedso the SDK mints its own, as legacy does. Mid-session consent changes are ignored until the next page load (legacy behavior:OptanonWrapperre-fires but the promise is already resolved).A new dependency-free
consent/settled.tsleaf carries the "has the CMP finished speaking" signal: pages without OneTrust settle immediately and send; pages where OneTrust never loads never settle and never send — both faithful to legacy, verified against the productionotSDKStub.jspipeline. The consent store itself becomes tri-state (true/false/absent per category) so an all-deny report is observable; the oldSetshape deduped it away silently.Legacy quirks replicated deliberately
logEvent(name, name, payload)keeps the event name in Statsig's value slot for dashboard continuity. Identity updates replicatesetUserPropertiesexactly, including its omission ofcustomIDs. Init-time user carriescustom.enabledExperimentsandgeRegion(read fromdata-ge-region, which the new Studio shell does not stamp yet — documented gap, readsnullthere). SDK-side storage is left enabled as in legacy.disableErrorLoggingwas dropped: it never existed in@statsig/js-clientand was dead config in the legacy reporter.Config and environments
The Statsig client key travels in the
app-configmeta tag (parsed bySiteConfigbesideobservability), replacing the legacyscript[data-statsig-api-client-key]transport forfrontend/pages. The transmit decision is server-side inStatsigAnalyticsHelper#analytics_config;AppConfigHelper#app_configcomposes the meta from the helpers that own each section, and the observability engine is untouched: production or managed-test emitsprovider: 'statsig', everything else'none'— the same environments legacy's client-sidelocal_modeallowed. Development gets a console-logging adapter mirroring legacy's local-mode log lines.Consumers
Studio registers the plugin (after
consentPlugin— order matters and is documented) and re-asserts identity from the router'sbeforeLoadwith a same-user short-circuit, so SPA navigations don't trigger redundantupdateUserAsyncrefetches. The users package fires the legacyAccount Settings Page Visitedevent at mount; the other five legacy account events belong to features (LTI unlink, roster sync, AI-diff toggle) whose UI does not exist in the new page yet, so they intentionally stay on the legacy page.Links
Testing story
Built TDD; 235 core / 186 users / 88 studio vitest tests pass (28 analytics, 27 consent), plus new Ruby tests for
analytics_configindashboard/test/helpers.yarn release:dryrun(build + lint + typecheck + test) green across all 68 turbo tasks; bothyarn.locks regenerated and--immutable-clean (apps consumes core via a yarn portal). An adversarial code review (15 confirmed findings) drove a hardening round: generation-guarded async boot, cookie-decode guards, explicit@statsig/js-clientpeer declarations, bundle-graph verification that the analytics chunk pulls no OneTrust/observability code. Parity was verified against legacy source line-by-line and against the production OneTrust stub's actual initialization pipeline.Not verifiable locally, needs a deployed environment: a real event landing in Statsig from a non-production deploy, and GDPR-geolocated banner behavior.
Deployment notes
No migrations or feature flags. Frontend Studio pages start reporting to Statsig on deploy (production/managed-test only), under the same event names and stable IDs legacy uses. Known gap:
geRegionreadsnullon the new Studio shell until it stampsdata-ge-regionon the root element like the legacy layout does.Privacy and security
No new PII and no change to what is collected: consent semantics are byte-for-byte parity with the legacy reporter (consent gates stable-ID persistence only, sending was never consent-gated), the stable-ID cookie contract is unchanged, and the Statsig SDK's own storage behavior is unchanged. The one behavioral tightening is bundle hygiene: the analytics subpath cannot drag the OneTrust provider or observability graph into consumer bundles.
🤖 Generated with Claude Code