Skip to content

feat: AnalyticsClient wiring + openURL public API [rn] (3/4)#36

Open
choudlet wants to merge 1 commit into
chrish/sc-36800/rn-lifecycle-pr2-emitterfrom
chrish/sc-36800/rn-lifecycle-pr3-client-wiring
Open

feat: AnalyticsClient wiring + openURL public API [rn] (3/4)#36
choudlet wants to merge 1 commit into
chrish/sc-36800/rn-lifecycle-pr2-emitterfrom
chrish/sc-36800/rn-lifecycle-pr3-client-wiring

Conversation

@choudlet

Copy link
Copy Markdown
Collaborator

Summary

Slice 3 of 4 in the RN lifecycle stack (sc-36800). Wires the storage foundation (#34) and emitter (#35) into `MetaRouterAnalyticsClient`, adds the public `openURL` API, and flips the `trackLifecycleEvents` default to opt-in.

Stacked on #35. Review the diff against that branch.

Behavior contract

  • Cold-launch state machine — detects fresh install vs SDK-upgrade vs version change vs no-op. Installed/Updated emits before the first `Application Opened` so attribution pipelines see install/update before session start.
  • Background-launched processes (push, headless JS) suppress the cold-launch Opened; the next `background → active` emits with `from_background: false` as the cold-launch bridge.
  • `inactive → active` (Control Center, FaceID, system alerts) is suppressed — only `background → active` emits Opened.
  • `Application Backgrounded` is enqueued before the flush-to-disk pass so the event ships in the same drain.
  • Deep links auto-captured via `Linking.getInitialURL()` (cold launch) and `Linking.addEventListener('url', ...)` (runtime). One-shot buffer with last-write-wins overwrite, cleared on next Opened emit.

Public API

```ts
analytics.openURL(url: string, sourceApplication?: string): void
```

Forwards a URL the host received from any source (Linking, UIScene URL handler, Android Intent, deep-link libraries that bypass Linking) so it's attached to the next `Application Opened`. No-op with a `Logger.warn` when `trackLifecycleEvents` is disabled — silent no-ops are bad DX. Wired through:

  • `AnalyticsInterface` (public type)
  • `proxyClient` (so pre-bind calls are queued and replayed on bind)
  • `init.ts` `boundClient`

Defaults flipped

`InitOptions.trackLifecycleEvents` now defaults to `false` (opt-in). Existing customers upgrading the SDK do not begin emitting these events without explicitly setting the flag. Matches iOS / Android v1.5.

App metadata consolidation

A single `appContext` snapshot is held on the client, populated once from `this.context.app` after `getContextInfo` resolves. Replaces three sites that independently re-derived `{version, build}` from the cached context — parity with the iOS `AppContext` consolidation. New `versionInfo()` private helper centralizes the read.

Test changes

  • New `disableDispatcherFlush` helper in the lifecycle `describe` block. Why: the dispatcher's `flush()` synchronously drains the in-memory queue inside its first `while` iteration (`drainBatch` runs before the `await`), which empties events emitted right before `this.flush()` in `handleAppStateChange`. Production behavior is correct (event ships via fetch); the helper keeps the queue snapshot intact for assertions.
  • 6 new `openURL` tests — buffered URL on next Opened, `sourceApplication` → `referring_application`, last-write-wins, one-shot clear, disabled no-op + warning, invalid input rejection.
  • 1 new test asserts the opt-in default (omitted flag → no events).
  • Pre-existing `preserves lifecycle storage across reset()` fixed — the AsyncStorage mock module shape is `{ default: { ... } }`, so the spy must target the `default` export.

Stack

  1. #34 — storage foundation
  2. #35 — emitter
  3. This PR — client wiring + `openURL` + opt-in default
  4. README documentation

Test plan

  • `npx jest src/analytics/MetaRouterAnalyticsClient.test.ts` — 87 tests pass
  • `npx jest src/analytics/proxy/proxyClient.test.ts` — passes
  • `npx tsc --noEmit` clean

Wires the storage foundation (#34) and emitter (#35) into
`MetaRouterAnalyticsClient` and adds the public `openURL` API.

Behavior

  - Cold-launch state machine: detects fresh install vs SDK-upgrade vs
    version change vs no-op, emits Installed/Updated *before* the first
    Application Opened so attribution pipelines see the install/update
    before the session start.
  - Background-launched processes (push, headless JS) suppress the
    cold-launch Application Opened; the next background→active emits
    with from_background:false as the cold-launch bridge.
  - inactive→active transitions (Control Center, FaceID, system alerts)
    are suppressed — only background→active emits Application Opened.
  - Application Backgrounded is enqueued *before* the dispatcher's
    flush-to-disk pass so the event ships in the same drain.
  - Auto-captures cold-launch URL via Linking.getInitialURL() and
    runtime URLs via Linking.addEventListener('url', ...). One-shot
    deep-link buffer with last-write-wins overwrite, cleared on the
    next Application Opened emit.

Public API

  - `openURL(url, sourceApplication?)` — forwards a URL the host
    received (Linking, UIScene URL handler, Android Intent, custom
    deep-link library) so it is attached to the next Application
    Opened. No-op with a Logger.warn when trackLifecycleEvents is
    disabled — silent no-ops are bad DX, hosts wiring this up should
    know they have the feature flag off.
  - Wired through AnalyticsInterface, the proxy (so pre-bind calls are
    queued), and init.ts boundClient.

Defaults

  - `trackLifecycleEvents` defaults to **false** (opt-in). Existing
    customers upgrading the SDK do not begin emitting these events
    without explicitly setting the flag — matches iOS / Android v1.5.

App metadata

  - Single `appContext` snapshot held on the client, populated once
    from `this.context.app` after `getContextInfo` resolves. Replaces
    three sites that independently re-derived `{version, build}` from
    the cached context — single source of truth, parity with the iOS
    `AppContext` consolidation.

Tests

  - `disableDispatcherFlush` helper added to the lifecycle describe
    block: the dispatcher's flush() synchronously drains the in-memory
    queue inside its first while iteration (drainBatch runs before the
    await), which empties events that were emitted right before
    `this.flush()` in handleAppStateChange. Production behavior is
    correct (event still ships via fetch); the helper just keeps the
    queue snapshot intact for assertions.
  - 6 new tests cover the openURL public API: buffered URL on next
    Opened, sourceApplication → referring_application, last-write-wins,
    one-shot clear, disabled no-op + warning, invalid input rejection.
  - 1 new test confirms the opt-in default (omitted flag → no events).
  - Pre-existing `preserves lifecycle storage across reset()` test
    fixed: AsyncStorage mock module shape is `{ default: { ... } }`,
    so the spy must target the `default` export, not the top object.

Slice 3 of 4 in the RN lifecycle stack (sc-36800).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant