Core: provider-agnostic analytics plugin with Statsig adapter (full legacy parity) - #74384
Open
stephenliang wants to merge 1 commit into
Open
Core: provider-agnostic analytics plugin with Statsig adapter (full legacy parity)#74384stephenliang wants to merge 1 commit into
stephenliang wants to merge 1 commit into
Conversation
stephenliang
marked this pull request as ready for review
August 4, 2026 22:55
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! |
Adds @code-dot-org/core/plugins/analytics so frontend/ packages can log product analytics without importing the legacy apps/ bundle. Module 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. Consent gates only stable-ID persistence, decided once per page load when the consent source settles; events send regardless. A page whose CMP never reports sends nothing; a page with no CMP sends immediately. The stable-ID cookie/localStorage contract is shared with other code.org pages and the Rails server-side session reader. Statsig config travels in the app-config meta, composed by FrontendAppConfigHelper from the section owners; production and managed-test servers transmit, development logs to the console. Studio registers the plugin and asserts identity from the router; the users package reports 'Account Settings Page Visited' at mount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stephenliang
force-pushed
the
stephen/core-analytics-plugin
branch
from
August 5, 2026 13:58
69e7a15 to
4004feb
Compare
stephenliang
changed the base branch from
staging
to
stephen/experiments-plugin
August 5, 2026 13:58
This was referenced Aug 5, 2026
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).The settlement signal (
whenConsentSettled, stack layer #74392) carries the "has the CMP finished speaking" state: 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 is unchanged (Set shape).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.enabledExperiments(viaplugins/experiments, stack layer #74393) andgeRegion(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