Repository navigation
feat: add provider-agnostic analytics layer - #161
devs-rootstrap wants to merge 2 commits into
Conversation
💯 Test Coverage
😎 Tests Results
👀 Tests Details • (59%)
|
There was a problem hiding this comment.
🟡 Changes recommended
Startup analytics can be lost because screen-tracker calls are not queued until each adapter finishes initialization.
1 open issue
What changed in this PR
Overall Review: ai:serious
Adds a vendor-independent analytics layer to the Expo template, keeping SDK calls out of screens and providing development logging without new dependencies.
Changes:
- Introduces typed events, an adapter contract, and a context API that dispatches calls to registered providers.
- Wires automatic route tracking and the default console adapter into the root provider tree.
- Adds tests and documentation for analytics usage and vendor integration.
| File | Description |
|---|---|
src/lib/index.tsx |
Re-exports analytics utilities and types. |
src/lib/analytics/types.ts |
Defines the event catalogue and adapter contract. |
src/lib/analytics/index.ts |
Exports the analytics module. |
src/lib/analytics/console-provider.ts |
Adds development-only analytics logging. |
src/components/providers/analytics.tsx |
Provides the context API and isolated provider dispatch. |
src/components/analytics-screen-tracker.tsx |
Tracks pathname changes automatically. |
src/app/_layout.tsx |
Registers analytics providers and screen tracking. |
docs/src/content/docs/guides/analytics.mdx |
Documents usage, typed events, and vendor integration. |
docs/astro.config.mjs |
Adds the analytics guide to navigation. |
AGENTS.md |
Updates the documented provider hierarchy. |
agent_docs/architecture.md |
Records analytics component and contract locations. |
__tests__/lib/analytics/console-provider.test.ts |
Tests development logging and production behavior. |
__tests__/components/providers/analytics.test.tsx |
Tests dispatch, failure isolation, and typed usage. |
__tests__/components/analytics-screen-tracker.test.tsx |
Tests initial and route-change screen tracking. |
Review effort: Balanced
| useEffect(() => { | ||
| fanOut(providers, async provider => provider.init()); | ||
| }, [providers]); |
There was a problem hiding this comment.
@asdolo Yes, it's a real issue worth fixing. React runs child effects before parent effects, so on first mount AnalyticsScreenTracker sends the initial screen view before AnalyticsProvider has even called init(). Nothing waits for an async init() either, so an SDK that needs setup (most vendor SDKs do) can drop the first screen view and any other early events.
The fix is ready in commit 7d211af (fix(analytics): queue provider calls until init resolves). Each provider now gets its own ready promise, and every call (trackEvent, trackScreen, identify, reset) waits on it, so a slow provider doesn't hold up the others. A provider whose init() fails is reported once and skipped after that. It adds a regression test that uses AnalyticsScreenTracker with a deferred init(), plus tests for a slow provider and for a failed init, and updates the analytics guide.
That commit isn't on the branch yet. The pre-push hook passes every check but shows the diff-size warning (the whole PR is about 780 changed lines against master, over the 700 limit), and that warning has to be confirmed by hand before the push goes through. The commit will show up here once the push is confirmed.

Jira board reference:
What does this do?
Adds a provider-agnostic analytics layer so app code never talks to a vendor SDK directly:
src/lib/analytics/types.ts: theAnalyticsProviderAdaptercontract (init,trackEvent,trackScreen, optionalidentify/reset) and a typed event catalogue (AnalyticsEventParams,AnalyticsEvent = keyof AnalyticsEventParams).trackEventis type-checked on both the event name and its params; events declared withundefinedparams can be tracked without a payload.src/components/providers/analytics.tsx:AnalyticsProvider(React context) that initialises every registered provider once on mount, plus theuseAnalytics()hook exposingtrackEvent,trackScreen,identifyandreset. Every call fans out to all providers; failures (sync throws or rejected promises) are isolated per provider and logged withconsole.error, so a broken SDK never crashes the app or blocks the other providers.src/components/analytics-screen-tracker.tsx: automatic screen tracking. It readsusePathname()from expo-router and callstrackScreen(pathname)on every route change.src/lib/analytics/console-provider.ts: built-inconsoleAnalyticsProviderthat logs in__DEV__and is a no-op otherwise. It is the default provider, so the template works out of the box with no vendor SDK and no native configuration.src/app/_layout.tsx: wiresAnalyticsProvider(insideAuthProvider) andAnalyticsScreenTracker; the provider list is a module-level constant.docs/src/content/docs/guides/analytics.mdx, registered in the Guides sidebar) explaining the layer, how to add an event to the catalogue, and how to plug in a real vendor with a Firebase / Google Analytics adapter sketch.AGENTS.mdprovider hierarchy andagent_docs/architecture.mdare updated accordingly.__tests__/components/providers/analytics.test.tsx(init once, fan-out, optional methods, failure isolation, compile-time catalogue checks, usage from a component),__tests__/components/analytics-screen-tracker.test.tsx(tracks on mount and on route change only) and__tests__/lib/analytics/console-provider.test.ts(dev logging, production no-op).Usage:
Why did you do this?
Analytics should be present in every project from the start, but the vendor is often chosen or replaced later. Keeping the vendor behind a small adapter interface means swapping or adding a provider is a one-file change, screens stay free of SDK calls, and the event catalogue gives a single typed source of truth for what the app tracks.
Who/what does this impact?
AnalyticsProvideris added betweenAuthProviderandBottomSheetModalProvider. No behaviour change for existing screens.@/libnow also re-exports the analytics module (consoleAnalyticsProviderand the analytics types).[analytics] ...lines for init, screen views and events. In production the default provider does nothing.package.jsonandpnpm-lock.yamlare untouched.Assumptions
google-services.json/GoogleService-Info.plist) and the@react-native-firebase/appconfig plugin, which would break builds in a template that does not ship them. The layer ships with the console provider and the guide documents how to add Firebase as a follow-up./feed,/settings). Projects that need different names can map them inside their adapter.Layer 2: Risk Classification
risk:medium: feature work touching the root layout and sharedsrc/libexports, with no changes to auth, payments or native code.How did you test this?
Verification
pnpm lint: passed (exit 0)pnpm type-check: passed (exit 0); this also validates the@ts-expect-errorassertions covering the typed event cataloguepnpm lint:translations: passed (exit 0)pnpm test: passed, 31 suites / 152 tests (exit 0)How to test locally
pnpm installandpnpm start:development, then open the app on a simulator or device.[analytics] initand[analytics] screen: /...; navigate between tabs and screens and a newscreen:line appears for each route change.useAnalytics().trackEvent('BUTTON_PRESS', { accessibilityId: 'some-id' })and check the[analytics] event: BUTTON_PRESS { accessibilityId: 'some-id' }line.trackEvent('NOT_IN_CATALOGUE', {}):pnpm type-checkfails, as intended.pnpm testto run the unit tests.Notes:
identify/resetare exposed by the hook but not yet called from the auth flow; wiring them to sign-in / sign-out can be done when a real provider is added.Screenshots / Previews
Not applicable: no UI changes.