@wireai/activation 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +62 -8
  2. package/CHANGELOG.md +188 -3
  3. package/INTEGRATION_PROMPT.md +25 -2
  4. package/README.md +112 -1
  5. package/dist/analytics/index.d.mts +18 -6
  6. package/dist/analytics/index.d.ts +18 -6
  7. package/dist/analytics/index.js +151 -41
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +147 -42
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/coachmarks/index.d.mts +16 -0
  12. package/dist/coachmarks/index.d.ts +16 -0
  13. package/dist/coachmarks/index.js +19 -13
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +19 -13
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-C0_odnIW.d.mts → currentSession-BlCeDP0f.d.mts} +145 -33
  18. package/dist/{currentSession-DdnUq2HQ.d.ts → currentSession-BxEB37xt.d.ts} +145 -33
  19. package/dist/index.d.mts +243 -53
  20. package/dist/index.d.ts +243 -53
  21. package/dist/index.js +531 -156
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +517 -157
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/questionnaire/index.d.mts +1 -1
  26. package/dist/questionnaire/index.d.ts +1 -1
  27. package/dist/questionnaire/index.js +79 -14
  28. package/dist/questionnaire/index.js.map +1 -1
  29. package/dist/questionnaire/index.mjs +79 -14
  30. package/dist/questionnaire/index.mjs.map +1 -1
  31. package/dist/reviews/index.d.mts +2 -2
  32. package/dist/reviews/index.d.ts +2 -2
  33. package/dist/reviews/index.js +103 -16
  34. package/dist/reviews/index.js.map +1 -1
  35. package/dist/reviews/index.mjs +103 -16
  36. package/dist/reviews/index.mjs.map +1 -1
  37. package/dist/showcase/index.js +15 -6
  38. package/dist/showcase/index.js.map +1 -1
  39. package/dist/showcase/index.mjs +15 -6
  40. package/dist/showcase/index.mjs.map +1 -1
  41. package/dist/{transport-BGW9uXZJ.d.mts → transport-CF_eHwzC.d.mts} +15 -1
  42. package/dist/{transport-jUJd5kxu.d.ts → transport-DsRe4epC.d.ts} +15 -1
  43. package/llms.txt +3 -0
  44. package/package.json +1 -1
  45. package/src/WireOnboarding.tsx +140 -5
  46. package/src/activation/useWireActivation.ts +12 -1
  47. package/src/activation/wireActivation.ts +44 -25
  48. package/src/analytics/analyticsFacade.ts +54 -29
  49. package/src/analytics/currentSession.ts +83 -0
  50. package/src/analytics/eventQueue.ts +9 -1
  51. package/src/analytics/index.ts +20 -1
  52. package/src/analytics/reportClientEvent.ts +42 -0
  53. package/src/analytics/screenTracking.ts +6 -1
  54. package/src/analytics/useAnalytics.ts +22 -1
  55. package/src/coachmarks/runtime.ts +53 -17
  56. package/src/config/wireConfigFromEnv.ts +46 -2
  57. package/src/context/deviceId.ts +173 -0
  58. package/src/context/userContext.ts +18 -0
  59. package/src/index.ts +54 -2
  60. package/src/questionnaire/runtime.ts +13 -2
  61. package/src/questionnaire/useQuestionnaireGate.ts +11 -4
  62. package/src/revenuecat/index.ts +55 -0
  63. package/src/revenuecat/purchaseEvents.ts +167 -0
  64. package/src/revenuecat/revenueCatBridge.ts +221 -0
  65. package/src/revenuecat/types.ts +95 -0
  66. package/src/reviews/runtime.ts +153 -1
  67. package/src/reviews/transport.ts +21 -2
  68. package/src/reviews/useReviewGate.ts +14 -4
  69. package/src/session-analytics/lifecycle.ts +9 -2
  70. package/src/session-analytics/reportSessionStart.ts +15 -4
  71. package/src/session-analytics/useLifecycleEvents.ts +83 -27
  72. package/src/session-analytics/useSessionStart.ts +37 -1
  73. package/src/types.ts +41 -4
package/AGENTS.md CHANGED
@@ -31,16 +31,14 @@ Use it whenever someone wants "AI / dynamic / personalized onboarding" in a Reac
31
31
  Everything an integration needs lives on the ROOT import (`@wireai/activation`). The other
32
32
  subpaths are optional secondary feature modules; import one only if you use it.
33
33
 
34
- - `@wireai/activation` (root): `WireOnboarding`, the helpers/types below, and the app-event/analytics reporters.
34
+ - `@wireai/activation` (root): `WireOnboarding`, the helpers/types below, and the device-only event reporters (`reportClientEvent`, `makeSessionId`, `WIRE_ONBOARDING_EVENTS`, see "App events" below).
35
+ - `@wireai/activation/analytics`: the analytics FACADE and its queue: `createAnalytics`, `useAnalytics`, `createScreenTracker`, `reportAppEvent`, `createEventQueue`, `clearUserContext`. These are NOT root exports; importing them from the root does not compile.
35
36
  - `@wireai/activation/coachmarks`: in-app coachmark tour (`CoachmarkProvider`, `useCoachmarkTour`).
36
37
  - `@wireai/activation/showcase`: post-onboarding feature showcase (`FeatureShowcase`).
37
38
  - `@wireai/activation/reviews`: store-review gating (`ReviewGate`, `ReviewModal`, `useReviewGate`, `fetchReviewDecision`).
38
39
  - `@wireai/activation/questionnaire`: in-app questionnaire gating (`QuestionnaireGate`).
39
40
  - `@wireai/activation/metro`: the `withWireOnboarding` Metro helper (see step 3).
40
41
 
41
- There is no `analytics` subpath: the app-event / analytics API is exported from the ROOT
42
- (see "App events / analytics" below).
43
-
44
42
  ## Public API (import from `@wireai/activation`)
45
43
 
46
44
  - `WireOnboarding`: the component (props below).
@@ -50,6 +48,10 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
50
48
  - `themeFromBrand({ primary })`: derive a full theme from one brand color.
51
49
  - `mergeTheme`, `defaultOnboardingTheme`, `OnboardingThemeProvider`, `useOnboardingTheme`.
52
50
  - `defaultIllustrations`: dependency-free fallback art; spread your own over it.
51
+ - `createRevenueCatBridge({ analytics, entitlementId })`: the RevenueCat purchase funnel (see "RevenueCat" below).
52
+ - `activationJoinContext(deviceKey)`: builds the `userContext` value that joins an onboarding session to the app's later events. Every `<WireOnboarding>` needs it (see step 7).
53
+ - `resolveAutoDeviceKey({ appId, storage })`: the kit's own persisted per-install `device_key`, for a host that owns none.
54
+ - `useLifecycleEvents(config, options)`: the app-root lifecycle hook (see step 8). `useSessionStart` / `reportSessionStart` are the counter-owning alternatives.
53
55
  - Types: `OnboardingTheme`, `OnboardingResult`, `OnboardingEvent`, `WireOnboardingConfig`, `WireOnboardingProps`, `StepValidator`, `OnboardingCopy`, `IllustrationRegistry`.
54
56
 
55
57
  ## `<WireOnboarding>` props
@@ -88,9 +90,10 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
88
90
  | `metadata` | `Record<string, unknown>` | no | Merged into every A2A request (e.g. install attribution). The kit reserves `sessionId` + `supportedComponents`; do not override them. |
89
91
  | `appVersion` | `string` | no | App version string (e.g. `"1.4.2"`); forwarded to the backend + client events so analytics can segment the funnel by app version. Auto-detected best-effort from `expo-constants`/`expo-application` (dependency-free) when omitted; pass it to override (yours wins). |
90
92
 
91
- ## App events / analytics (root export)
93
+ ## App events (root export)
92
94
 
93
- The app-event / analytics API is exported from the ROOT (`@wireai/activation`), NOT a subpath.
95
+ These device-only event helpers are exported from the ROOT (`@wireai/activation`). The analytics
96
+ FACADE (`createAnalytics` and friends) is a different surface and lives on `@wireai/activation/analytics`.
94
97
 
95
98
  - `WIRE_ONBOARDING_EVENTS` + `toAnalyticsEvent(event)`: canonical `wire_onboarding_*` funnel names; log via the app's own analytics.
96
99
  - `reportClientEvent(target, event)` / `reportClientEvents(target, events)`: report device-only funnel events. Contract: `POST {serverUrl}/v1/events`, header `Authorization: Bearer {apiKey}`, body `{ "events": [ ... ] }`. `target = { serverUrl, apiKey }` is derived from `WireOnboardingConfig`. Fire-and-forget: never throws into the UI.
@@ -105,8 +108,9 @@ The app-event / analytics API is exported from the ROOT (`@wireai/activation`),
105
108
  4. **Config**: set `EXPO_PUBLIC_WIREAI_API_KEY` / `_SERVER_URL` / `_APP_ID`; build via `wireConfigFromEnv({ appId })`.
106
109
  5. **Fonts**: host must load the font families named in `theme.fonts.regular/medium/bold` (e.g. `expo-font`), else text falls back to system font.
107
110
  6. **Theme**: `themeFromBrand({ primary })` or a full `Partial<OnboardingTheme>`; for dark/light, pick the theme by the app's theme state.
108
- 7. **Render + continuation**: drop `<WireOnboarding>` into the signup flow; `onComplete` → persist + navigate, `onSkip` → navigate.
109
- 8. **Gate**: wrap behind a flag (env for dev, remote config for prod) **AND** `config != null`; fall through to existing onboarding when off/unconfigured. Decide who sees it (e.g. new signups only) in host nav logic.
111
+ 7. **Render + continuation**: drop `<WireOnboarding>` into the signup flow; `onComplete` → persist + navigate, `onSkip` → navigate. Pass the join key: `userContext={activationJoinContext(deviceKey)}`, or `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))` when the app owns no device id. Skipping it leaves the `activated` funnel permanently empty and reports no error.
112
+ 8. **Lifecycle (do not skip)**: mount `useLifecycleEvents(config, { deviceKey?, sessionCount?, userId? })` ONCE at the app root, before anything else touches analytics. It is the only path that emits `app.first_open`, it registers the per-open session id that `createAnalytics` / `wire.track` / the gates all correlate to, and with `config.storage` it stamps the kit's persisted auto `device_key` on `app.session_started`, which is what the server's `min_sessions` rule counts. `useSessionStart` / `reportSessionStart` are the alternatives for a host that already owns an open counter; neither emits `first_open`.
113
+ 9. **Gate**: wrap behind a flag (env for dev, remote config for prod) **AND** `config != null`; fall through to existing onboarding when off/unconfigured. Decide who sees it (e.g. new signups only) in host nav logic.
110
114
 
111
115
  ## Review firing (do not hand-roll the decision fetch)
112
116
 
@@ -131,6 +135,56 @@ write your own fetch.
131
135
  `createWireActivation(config)`. This is the kit-owned replacement for hand-rolling
132
136
  session-id + await-POST + revalidate.
133
137
 
138
+ ## RevenueCat (the purchase funnel)
139
+
140
+ The app sells subscriptions through `react-native-purchases`? Wire the paywall to the same event
141
+ stream as onboarding. The kit does NOT depend on `react-native-purchases` (it is a native module and
142
+ the kit never forces a rebuild); the adapter types the RevenueCat objects structurally, so you pass
143
+ the real ones you already have.
144
+
145
+ ```tsx
146
+ import { createRevenueCatBridge, activationJoinContext } from "@wireai/activation";
147
+ import { createAnalytics } from "@wireai/activation/analytics";
148
+
149
+ const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userContext: { deviceKey } });
150
+ const revenuecat = createRevenueCatBridge({ analytics, entitlementId: "pro" });
151
+
152
+ revenuecat.paywallShown(offering, { source: variant });
153
+ revenuecat.checkoutStarted(pkg, { source: variant });
154
+ const entitled = revenuecat.purchaseCompleted(customerInfo, pkg, { source: variant });
155
+ revenuecat.purchaseFailed(error, pkg, { source: variant });
156
+ revenuecat.purchasesRestored(customerInfo);
157
+ revenuecat.syncPlanTier(customerInfo); // at launch, writes plan_tier with no event
158
+ ```
159
+
160
+ Canonical names (they are also the `question_key` strings a firing trigger matches on):
161
+ `wire_paywall_shown`, `wire_checkout_started`, `wire_purchase_completed`, `wire_purchase_failed`,
162
+ `wire_purchase_restored`.
163
+
164
+ **The join key is `user_context.device_key`, never `session_id`.** An onboarding session id is the
165
+ A2A `contextId` and an app-event session id is the per-open id; the two live in separate spaces on
166
+ purpose, so intersecting them returns nothing. Put the SAME device key on both sides: the analytics
167
+ and activation surfaces auto-mint and stamp it on every event, and onboarding gets it from
168
+ `<WireOnboarding userContext={activationJoinContext(deviceKey)} ... />`. Never hand-write
169
+ `userContext={{ deviceKey }}`, because the server's device lookup reads `device_key` and a
170
+ misspelled bucket produces an empty funnel instead of an error.
171
+
172
+ **If the host owns no device id, read the kit's.** Do not leave the onboarding side blank.
173
+ `resolveAutoDeviceKey({ appId, storage })` returns the exact id `createAnalytics` /
174
+ `createWireActivation` auto-mint and persist for this install, so both halves of the join agree:
175
+
176
+ ```tsx
177
+ import { resolveAutoDeviceKey, activationJoinContext } from "@wireai/activation";
178
+
179
+ const deviceKey = resolveAutoDeviceKey({ appId, storage }); // the SAME id the analytics side stamps
180
+ <WireOnboarding config={config} userContext={activationJoinContext(deviceKey)} ... />
181
+ ```
182
+
183
+ Always pass `storage`. Without it the id is per-LAUNCH, not per-install, and a per-launch key makes
184
+ every open look like a new device, which breaks `min_sessions` and A/B arm stickiness as surely as
185
+ no key breaks the join. The same rule governs the lifecycle wiring: `useLifecycleEvents` falls back
186
+ to this id for `app.session_started` / `app.first_open` ONLY when `config.storage` is present.
187
+
134
188
  ## Gotchas (do not miss)
135
189
 
136
190
  - **EAS / cloud builds:** install via **git URL or registry**, never a local `file:` path *outside the app repo* (EAS won't resolve it).
package/CHANGELOG.md CHANGED
@@ -3,10 +3,195 @@
3
3
  All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
4
4
  Historical entries below the rename keep the old package name on purpose.
5
5
 
6
- ## [Unreleased]
6
+ ## [0.12.2] — Unreleased
7
+
8
+ The auto-join patch. 0.12.1 taught the kit to complain about a missing join key; this one teaches it
9
+ to supply the key. `user_context.device_key` is the only thing that stitches an onboarding session to
10
+ everything the app reports later, the prop that carries it is optional, and two of three production
11
+ integrations shipped without it and read `activated: 0`. A warning fixes that for the next host, not
12
+ for the current one, so the default now produces a joined funnel.
13
+
14
+ ### Changed
15
+
16
+ - **⚠️ BEHAVIOR CHANGE: `<WireOnboarding>` supplies its own join key when the host supplied none.**
17
+ With `storage` present and no `device_key` in `userContext`, the kit merges in its own per-install
18
+ key, and both the A2A session-start metadata and the onboarding client events carry it. That key is
19
+ the SAME id `createAnalytics` / `createWireActivation` mint and persist, resolved through
20
+ `hydrateAutoDeviceKey({ appId, storage })` rather than the synchronous
21
+ `resolveAutoDeviceKey({ appId, storage })`, so it is the persisted id, not a pre-hydration mint.
22
+ Three limits, all deliberate:
23
+ - **A host-supplied key always wins**, forwarded verbatim, nothing merged over it. An integration
24
+ that already passes `activationJoinContext(deviceKey)` sends a byte-identical payload.
25
+ - **No `storage`, no injection.** Without persistence the id is minted fresh every launch, and the
26
+ server counts `min_sessions` by distinct app-opens grouped on `device_key`: a per-launch key
27
+ corrupts that counter instead of merely leaving the join empty. The kit declines and warns in dev.
28
+ - **The key is resolved BEFORE the provider mounts**, behind the loader gate the persisted-session
29
+ read already holds. `userContext` feeds the A2A config, and swapping it after mount would rebuild
30
+ the adapter and drop the server-learned `contextId`. The read is timeout-capped, so a hung storage
31
+ adapter degrades to no key instead of a stuck loader.
32
+
33
+ Who this changes: a host that deliberately wanted an unlinked onboarding session. That host now
34
+ passes `autoJoinKey={false}`.
35
+ - **The 0.12.1 missing-join-key dev warning is reconciled with the above.** It no longer fires when
36
+ auto-injection covered the gap. It still fires when the gap is genuinely open, and it now names
37
+ which of the three reasons applies: no `storage`, `autoJoinKey={false}`, or a storage adapter that
38
+ did not answer in time.
39
+ - **A hand-written `userContext={{ deviceKey }}` still warns, even though the kit injected.** That
40
+ host demonstrably owns a device id, and their app-side events carry it, so the two families still
41
+ land in disjoint id spaces. Injection makes the onboarding side self-consistent; only the host can
42
+ make it join. Silencing this would have undone the 0.12.1 fix for the most common mis-wiring.
43
+
44
+ ### Added
45
+
46
+ - **`autoJoinKey` prop on `<WireOnboarding>`,** the documented opt-out. Default `true`. Setting it to
47
+ `false` restores the pre-0.12.2 behavior exactly: nothing is injected and the dev warning fires.
48
+ Documented in the `userContext` JSDoc, the README props table, and the README join-key section.
49
+
50
+ ## [0.12.1] — 2026-07-27
51
+
52
+ The integration-contract patch. 0.12.0's runtime worked; what did not work was everything the package
53
+ let a host leave out. Two of three real integrations went wrong in ways the kit accepted without
54
+ complaint, and the review that produced this release found a third: the ordered integration steps in
55
+ every shipped doc never once said "mount `useLifecycleEvents`", while the kit's own runtime warning
56
+ names that exact symbol as the fix. Nothing here tightens the wire; every new check is `__DEV__`-only
57
+ or a client-side default.
58
+
59
+ ### Added
60
+
61
+ - **`<WireOnboarding>` warns in dev when it got no join key.** `user_context.device_key` is the only
62
+ thing that joins an onboarding session to the app's later events, and the prop that carries it is
63
+ optional and named `userContext`, so forgetting it is the default and the failure is a silent zero
64
+ in the `activated` funnel. The warning names `activationJoinContext(deviceKey)` and, for a host that
65
+ owns no device id, `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`. The
66
+ `userContext` JSDoc + the README props row now lead with "this is where the join key goes" instead
67
+ of describing the prop as segmentation context. The prop is NOT renamed (breaking, for no gain), and
68
+ auto-injection is deliberately held for 0.13.0.
69
+ - **`wireConfigFromEnv` says "Wire is off" out loud in dev.** A missing EAS env var made it return
70
+ `null`, and then the facade never constructed, `useLifecycleEvents` no-oped, and every gate returned
71
+ null: three independent silent no-ops from one missing var. It now warns once, naming WHICH var is
72
+ missing and that the kit is fully disabled. The `null` return is unchanged, and hosts still gate on
73
+ it. `WIRE_ENV_VARS` is exported so a host preflight can assert the three names itself.
74
+ - **`hydrateAutoDeviceKey({ appId, storage })`,** the awaitable sibling of `resolveAutoDeviceKey`. It
75
+ joins the same single-flight storage read and resolves once the persisted id is in hand, for the one
76
+ caller that can afford to wait. Never rejects.
77
+ - **The review / questionnaire gates warn in dev when they have no sync storage.** With no storage the
78
+ app-open counter is pinned at 1 forever, so the fail-closed `minSessions: 2` default is
79
+ unsatisfiable and the gate never fires, which is indistinguishable from "the rules said not yet".
80
+ - **The lifecycle mount reached the docs.** `useLifecycleEvents` is now an ordered step in `AGENTS.md`
81
+ and `INTEGRATION_PROMPT.md`, a README section directly above "Session mapping", and a line in
82
+ `llms.txt`. `useSessionStart` / `reportSessionStart` are demoted to the "you already own a counter"
83
+ alternatives, with the fact that neither emits `app.first_open` stated where a reader will hit it.
84
+
85
+ ### Fixed
86
+
87
+ - **`useLifecycleEvents` stamped a freshly minted `device_key`, not the persisted one.**
88
+ `resolveAutoDeviceKey` is synchronous by contract, so a mount-time caller got a brand-new `wdev_*`
89
+ and adopted the persisted id milliseconds later. On every launch, both lifecycle events therefore
90
+ rode a key nothing else shared. The server computes `min_sessions` by counting distinct
91
+ `app.session_started` grouped by `device_key`, so that counter could never exceed 1, and
92
+ `app.first_open` (once ever) landed under a key no later event shares, which broke `first_open` to
93
+ `activated` cohorting too. The mount fire now awaits `hydrateAutoDeviceKey` — but ONLY when the auto
94
+ fallback is actually in play. A host-supplied `deviceKey`, or no `storage`, still fires
95
+ synchronously at mount.
96
+ - **The gate counter's unit could change mid-launch.** The gates call `bumpSessionCount` from a
97
+ `useState` initializer (render phase) while `useLifecycleEvents` registers the per-open id from a
98
+ root effect, and React runs every render before any effect. So a cold start counted a process id and
99
+ then a session id as TWO app-opens, which made `minSessions: 2` — the fail-closed default added
100
+ after the 2026-07-16 one-star incident — satisfiable inside a single launch. `currentOpenId()` now
101
+ pins whatever it first resolved for the whole launch. Behavioural, in the fail-closed direction: a
102
+ gate fires later, never earlier.
103
+ - **The coachmark storage singleton was a plain module `let`, duplicated across four dist bundles.**
104
+ Under `dist` resolution (node `import`/`require`, SSR, jest, RN-web, anything that is not Metro) a
105
+ host that mounted `CoachmarkProvider` from `@wireai/activation/coachmarks` wrote the coachmarks copy
106
+ while `useReviewGate` from `@wireai/activation/reviews` read the reviews copy, which was still
107
+ `null` — so the gate never fired, the `seen` once-gate never persisted, and a tenant's coachmarks
108
+ kill switch never reached the reviews bundle. The storage, the QA replay flag and the kill switch now
109
+ live in one `globalThis` slot keyed by `Symbol.for("@wireai/activation:coachmarkRuntime")`, the same
110
+ remedy already applied to `currentSession`, the auto device-key registry and activation revalidation.
111
+ - **The analytics facade read the session registry but never registered.** `resolveSessionId` was a
112
+ pure read with a per-instance fallback, so a `track` before the root lifecycle effect carried an id
113
+ the server had no `app.session_started` for and the server back-filled a phantom session, while a
114
+ `wire.track` moments later minted and registered a different one. It is now
115
+ `config.sessionId ?? ensureCurrentSessionId()`, a strict superset: when an open is registered this is
116
+ exactly `getCurrentSessionId()`, and the frozen-id opt-out is untouched.
117
+ - **`wireConfigFromEnv` structurally could not carry `appVersion`,** so a host that built its config
118
+ the documented way forwarded `undefined` everywhere. The facade and lifecycle paths each auto-detect
119
+ their own fallback; `createWireActivation` did not, so EVERY `wire.track` event shipped with no
120
+ `app_version` at all. The helper now defaults it to `detectAppVersion()` (an explicit override still
121
+ wins) and `createWireActivation` gained the same fallback as the facade.
122
+ - **`useSessionStart` had no auto `device_key` fallback,** while its sibling `useLifecycleEvents` did.
123
+ A host on the documented `useSessionStart` path emitted the very event `min_sessions` counts with no
124
+ device key while its other events carried an auto-minted one. Backported, storage-gated on the same
125
+ rule (no storage means no fallback, because a per-launch key corrupts the counter the other way).
126
+ - **`AGENTS.md` shipped an import that cannot resolve, and denied a subpath that exists.**
127
+ `import { createAnalytics } from "@wireai/activation"` does not compile (it lives on
128
+ `@wireai/activation/analytics`), and the same file claimed "There is no `analytics` subpath" while
129
+ `package.json` exports it, `tsup` builds it and `llms.txt` documents it. `README.md` shipped the same
130
+ broken import. Both are fixed, and `src/docsContract.test.ts` now resolves every documented
131
+ specifier against the BUILT `dist` through the real `exports` map, so the drift cannot return quietly.
132
+ - **`warnOnSkippedEvents` parsed the response body in release builds.** The `__DEV__` check sat inside
133
+ the `.then`, after `res.json()`, so every persistent-path POST paid a JSON parse to build a warning
134
+ nobody would read. The check is now the first statement in the function.
135
+
136
+ ## [0.12.0] — 2026-07-27
137
+
138
+ The release Myelino 2.2.0 pins. Ships the RevenueCat path (#52), the identity/counting audit,
139
+ the session-id fallback contract, and the transport/config fixes behind a silently dead lifecycle
140
+ stream (a consumer's `first_open` read 6 all-time while the emitting code was deployed).
141
+
142
+ ### Added
143
+
144
+ - **RevenueCat path (`createRevenueCatBridge`).** The purchase funnel is now a first-class, documented
145
+ path instead of glue every consumer writes itself. New ROOT exports: `createRevenueCatBridge`,
146
+ `WIRE_PURCHASE_EVENTS` (`wire_paywall_shown`, `wire_checkout_started`, `wire_purchase_completed`,
147
+ `wire_purchase_failed`, `wire_purchase_restored`), the pure mappers (`activeEntitlement`,
148
+ `resolvePlanTier`, `describePackage`, `describeEntitlement`, `isUserCancelled`, `describeFailure`),
149
+ `PLAN_TIER_CONTEXT_KEY`, and the structural RevenueCat types. The bridge reports through the
150
+ `createAnalytics` or `createWireActivation` instance the host already holds (a compile-time check in
151
+ `revenueCatBridge.ts` keeps both assignable), writes `plan_tier` (paid / trial / free) to the bound
152
+ user context, drops the store's localized error `message` and keeps only the stable RevenueCat
153
+ `code`, and reports a store-confirmed purchase that granted no entitlement as
154
+ `wire_purchase_failed` with `reason: "not_entitled"` rather than swallowing it. **No new dependency:
155
+ `react-native-purchases` is a native module and is never imported**, so the RevenueCat objects are
156
+ typed structurally and the host passes its real ones.
157
+ - **`activationJoinContext(deviceKey)`.** The `userContext` value that joins an onboarding session to
158
+ the app's later events. The join key is `user_context.device_key` and NEVER `session_id`: an
159
+ onboarding session id is the A2A `contextId`, an app-event session id is the per-open id, and
160
+ intersecting the two id spaces returns zero rows every time. This helper exists so the wire spelling
161
+ (`device_key`) is decided in one place; a hand-written `userContext={{ deviceKey }}` builds a bucket
162
+ the server's device lookup does not read, which produces a silently empty funnel rather than an
163
+ error. `src/revenuecat/joinKey.test.ts` locks both sides against each other.
164
+ - **One shared auto `device_key` per install.** `createAnalytics` and `createWireActivation` each
165
+ minted their OWN id and raced a storage read, so a host building both (the documented wiring)
166
+ stamped two different `device_key`s on one install — splitting the key that `min_sessions`, A/B arm
167
+ stickiness and the purchase-to-onboarding join all read. Now ONE id lives in a `globalThis` slot
168
+ keyed by `Symbol.for(...)` (the `currentSession` pattern). `useLifecycleEvents` now stamps the
169
+ shared key on lifecycle events too (storage-gated, so a per-launch id never corrupts the counter),
170
+ and `resolveAutoDeviceKey` is exported from the root and `./analytics` barrels.
171
+
172
+ ### Fixed
7
173
 
8
- Architecture-audit fixes. The next release that ships these MUST be at least **0.11.0** (0.10.0 is
9
- published and immutable). One BEHAVIOR CHANGE, called out below.
174
+ - **Every event carries a `session_id`, or the server eats it.** `POST /v1/events` validates per
175
+ event inside a try/except that counts the failure as "skipped" and returns HTTP 200 anyway, so an
176
+ event without a session id was accepted by the wire and discarded. `ensureCurrentSessionId()`
177
+ returns the registered per-open id when there is one and otherwise mints one, REGISTERS it so later
178
+ events join the same session, and warns once per process in `__DEV__` (mount `useLifecycleEvents`
179
+ at the app root). Closes the `reportAppEvent(target, "screen", { deviceKey })` hole and
180
+ `wire.track()`'s refusal to send without a registered open.
181
+ - **`config.sessionId` on `createAnalytics` now warns in dev.** Passing it pins EVERY event from the
182
+ instance to that one frozen id and opts out of the live per-open session — the exact footgun that
183
+ flatlined a consumer's lifecycle metrics. The JSDoc and README now state the freeze semantics
184
+ honestly; omitting it is the correct default.
185
+ - **The persistent transport paths read the `/v1/events` ACK body.** A 200 was never a receipt: the
186
+ server reports discarded events only as `skipped` in the body. The event queue and
187
+ `reportSessionStart` now warn in dev with the skipped count. Log-only — retry/dequeue semantics
188
+ unchanged.
189
+ - **`user_context.app_version` falls back to `detectAppVersion()`** when the host passes nothing, so
190
+ version attribution on the facade path no longer depends on the host forwarding its own version.
191
+
192
+ ## [0.11.0] — 2026-07-21
193
+
194
+ Architecture-audit fixes (#50/#51). One BEHAVIOR CHANGE, called out below.
10
195
 
11
196
  ### Changed (BEHAVIOR) — review gate no longer fires on the first session
12
197
 
@@ -40,10 +40,21 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
40
40
  - `fallbackFlow` MUST be my existing static onboarding so a backend error/timeout degrades, never
41
41
  dead-ends.
42
42
  - Forward install attribution if I have it: `wireConfigFromEnv({ appId, metadata: attributionMetadata({...}) })`.
43
+ - THE JOIN KEY: pass `userContext={activationJoinContext(deviceKey)}`. If my app owns no stable
44
+ device id, use `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`, which returns
45
+ the same id the analytics side stamps. Leaving it out makes the `activated` funnel read zero
46
+ forever and reports no error.
43
47
  - Analytics: log via `toAnalyticsEvent(e)` → my logger, using the canonical `wire_onboarding_*` names.
44
48
  7. Navigation: mount the screen in my first-run/signup flow; branch to the static flow when the gate
45
49
  is off or config is missing.
46
- 8. Event triggers (only if I asked to fire a review/questionnaire off an in-app action): pick the
50
+ 8. Lifecycle (do NOT skip): mount `useLifecycleEvents(config, { deviceKey, sessionCount, userId })`
51
+ ONCE at my app root, above the navigator, before anything else touches analytics. Pass
52
+ `config.storage`. It is the only path that emits `app.first_open`, it registers the per-open
53
+ session id every other surface correlates to, and with storage it stamps the kit's persisted auto
54
+ `device_key` on `app.session_started`, which is what the server counts `min_sessions` from.
55
+ `useSessionStart` / `reportSessionStart` are only for a host that already owns an open counter;
56
+ neither emits `first_open`.
57
+ 9. Event triggers (only if I asked to fire a review/questionnaire off an in-app action): pick the
47
58
  action that should trigger it (e.g. finishing a task, journaling a win) and its key, e.g.
48
59
  `journal_done`. On my home/gate screen call
49
60
  `const { track, sessionId, revalidation } = useWireActivation({ serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY, appId: APP_ID, deviceKey })`
@@ -53,7 +64,19 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
53
64
  `useQuestionnaireGate` the same way), and at the action site call `await track("journal_done")`.
54
65
  Do NOT hand-roll the session id or the POST; `wire.track` owns both and revalidates the gate on
55
66
  a successful 2xx.
56
- 9. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
67
+ 10. RevenueCat (only if my app sells subscriptions through `react-native-purchases`): wire my paywall
68
+ to the same event stream with `createRevenueCatBridge({ analytics, entitlementId: "<my
69
+ entitlement, e.g. pro>" })` from `@wireai/activation`. Do NOT add or import
70
+ `react-native-purchases` in the kit path; the adapter types the RevenueCat objects structurally,
71
+ so pass my real `PurchasesOffering` / `PurchasesPackage` / `CustomerInfo` straight in. Replace my
72
+ hand-rolled paywall analytics calls with `paywallShown` / `checkoutStarted` / `purchaseCompleted`
73
+ / `purchaseFailed` / `purchasesRestored`, and call `syncPlanTier(customerInfo)` once at launch.
74
+ THE JOIN KEY: pass the SAME device key to the analytics instance AND to onboarding via
75
+ `<WireOnboarding userContext={activationJoinContext(deviceKey)} />`, because purchases join to
76
+ onboarding on `user_context.device_key` and never on `session_id` (those are separate id spaces
77
+ and intersecting them returns zero rows). If my app has no single stable device key yet, say so
78
+ instead of inventing one.
79
+ 11. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
57
80
 
58
81
  Report back: the files you changed (path:line), the typecheck result, and anything you couldn't
59
82
  infer about my conventions.
package/README.md CHANGED
@@ -246,7 +246,8 @@ import { WireOnboarding } from "@wireai/activation";
246
246
  | `storage` | `WireOnboardingStorage` | Host-injected storage (AsyncStorage-compatible `getItem/setItem/removeItem`) for **session-id persistence**: an app KILL mid-onboarding resumes the SAME backend session instead of minting a new one, so the funnel's `started` count stays honest (no phantom drops). Pass AsyncStorage as-is, or a 3-line MMKV wrapper. Omit for the previous per-mount behavior. Persists the kit's own correlation seed only — answers stay the host's job via `onComplete`. |
247
247
  | `sessionTtlMs` | `number` | How long a persisted session id stays resumable. Default `3600000` (1h, the backend's session TTL). With `storage` only. |
248
248
  | `persistKey` | `string` | Override the storage key (default `wireai:session:<appId>`). Scope per-user if one device can onboard multiple accounts mid-flow. With `storage` only. |
249
- | `userContext` | `Record<string, string \| number \| boolean>` | Host-injected, non-PII context the app already knows (signup method, referral, plan, a HASHED user id). Forwarded on session metadata + client events for funnel segmentation. **No PII on THIS prop** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). ⚠️ Distinct from the analytics/activation **`WireUserContext`** object (`createAnalytics` / `useWireActivation`), which additionally accepts an opt-in raw `userEmail` — see [Rich user context & PII](#rich-user-context--pii). |
249
+ | `userContext` | `Record<string, string \| number \| boolean>` | **This is where the join key goes.** `user_context.device_key` is the only thing that joins this onboarding session to the app's later events, so pass `userContext={activationJoinContext(deviceKey)}` (or `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))` if your app owns no device id). Since 0.12.2, omitting it no longer empties the funnel silently: with `storage`, the kit injects its own key (see [The join key](#the-join-key-device_key-never-session_id) and `autoJoinKey` below). Second job: any other non-PII context the app already knows (signup method, referral, plan, a HASHED user id), forwarded on session metadata + client events for funnel segmentation. **No PII on THIS prop** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). ⚠️ Distinct from the analytics/activation **`WireUserContext`** object (`createAnalytics` / `useWireActivation`), which additionally accepts an opt-in raw `userEmail` — see [Rich user context & PII](#rich-user-context--pii). |
250
+ | `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present, and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you passed, and it cannot inject without `storage` (see [The join key](#the-join-key-device_key-never-session_id)). |
250
251
  | `userId` | `string` | Your own OPAQUE user id, so onboarding sessions reconcile to real users later (console sessions to your user table / GA4 users). Optional and supports **late binding**: present at mount it rides the session-start metadata; if it changes mid-session (the user just registered) the kit emits an `identify` event; available only after the flow, use `identifyOnboarding(...)`. **No PII** (not an email/name/phone); trimmed and capped at 128 chars. See [User identity](#user-identity). |
251
252
 
252
253
  ## Helpers (the reusable substrate)
@@ -370,6 +371,52 @@ await clearUserContext({ storage, appId }); // the wire/activation path (then re
370
371
 
371
372
  **3. The auto `device_key` is a per-install identifier.** When you supply no `deviceKey`, the analytics and activation surfaces auto-mint a stable per-install `device_key`, persist it via `storage` (`wireai:analytics:deviceKey:<appId>`), and stamp it on every event so the server's review/questionnaire gating and A/B stickiness work out of the box. It carries no hardware id, no IDFA/GAID, and cannot be joined across apps (the privacy category of a first-party cookie), but it IS a persistent per-install id. If you adopt these surfaces, declare it in your App Privacy / Data Safety accordingly. Supply your own `deviceKey` to override it.
372
373
 
374
+ ### Do not pass `sessionId` to `createAnalytics`
375
+
376
+ `CreateAnalyticsConfig.sessionId` is an opt-out knob, not a default. Set it and **every** event that
377
+ instance sends (including `identify`) is pinned to that one frozen id, and the instance stops
378
+ following the live per-open session the kit registers from `app.session_started` (see
379
+ [Session mapping](#session-mapping-know-when-a-user-opens-the-app-again)). Lifecycle analytics then
380
+ collapse onto a single device-scoped id: one "first open" for the life of the install, however many
381
+ times the user comes back. The kit warns about it in dev builds.
382
+
383
+ ```tsx
384
+ const analytics = createAnalytics({ serverUrl, apiKey, storage, appId }); // ✅ follows each app-open
385
+ const analytics = createAnalytics({ serverUrl, apiKey, sessionId: myId }); // ⚠️ frozen for good
386
+ ```
387
+
388
+ Omit it. Pass one only if your host runs its own session lifecycle and owns the id the server should
389
+ correlate on. This is a different field from the per-open `sessionId` that `useWireActivation` and
390
+ `reportAppEvent` READ (that one is the live id, and reading it is always fine).
391
+
392
+ ## Lifecycle events (mount this once, before anything else touches analytics)
393
+
394
+ One hook at your app root wires the whole top of the funnel: `app.first_open` once ever, and
395
+ `app.session_started` on every real app-open, both offline-buffered.
396
+
397
+ ```tsx
398
+ import { useLifecycleEvents } from "@wireai/activation";
399
+
400
+ // At the app root, above your navigator:
401
+ useLifecycleEvents(
402
+ { serverUrl, apiKey, appId, storage }, // same creds as onboarding
403
+ { deviceKey, sessionCount, userId }, // all optional
404
+ );
405
+ ```
406
+
407
+ Mount it first. Three other things read the per-open session it registers: `createAnalytics`,
408
+ `wire.track`, and the review / questionnaire gates. Without it every one of them falls back to an id
409
+ the server has no `app.session_started` for, so the session is thinner than a real open, and the kit
410
+ warns about it once in dev.
411
+
412
+ Pass `storage`. With it the hook falls back to the kit's persisted auto `device_key` when you supply
413
+ none, and waits for that key to be read back before it fires, so the events the server counts
414
+ `min_sessions` from carry the id this install keeps. Without storage the id would be per-launch, which
415
+ would make every open look like a new device, so the fallback deliberately does not engage.
416
+
417
+ The two alternatives below are for a host that already owns an open counter. Both emit
418
+ `app.session_started` only, so `app.first_open` stays empty forever on those paths.
419
+
373
420
  ## Session mapping (know when a user opens the app again)
374
421
 
375
422
  Onboarding runs once, on the first launch. Session mapping is the other half: every time the
@@ -965,6 +1012,70 @@ If you are outside React (a service, a saga), `createWireActivation(config)` is
965
1012
  factory. It returns `{ track, sessionId, subscribeRevalidation, getRevalidationVersion }`: the
966
1013
  same `track`, plus the raw pub/sub the hook wraps for you.
967
1014
 
1015
+ ## RevenueCat (the purchase funnel)
1016
+
1017
+ If you sell subscriptions with [RevenueCat](https://www.revenuecat.com/), the paywall is the other end of the funnel onboarding starts. `createRevenueCatBridge` wires the two together: one constructor, then your existing paywall call sites.
1018
+
1019
+ The kit does **not** depend on `react-native-purchases`, and it never will. That package is a native module, and the kit's whole promise is that it never puts a native rebuild in your way. So the adapter types the RevenueCat objects structurally instead, which means you hand it the real `CustomerInfo` / `PurchasesPackage` / `PurchasesOffering` you already have. Nothing new gets installed.
1020
+
1021
+ ```tsx
1022
+ import { createRevenueCatBridge } from "@wireai/activation";
1023
+ import { createAnalytics } from "@wireai/activation/analytics";
1024
+
1025
+ const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userContext: { deviceKey } });
1026
+ const revenuecat = createRevenueCatBridge({ analytics, entitlementId: "pro" });
1027
+ ```
1028
+
1029
+ Then five call sites, all fire-and-forget, none of which can throw at your paywall:
1030
+
1031
+ ```tsx
1032
+ revenuecat.paywallShown(offering, { source: variant }); // offering_id + packages_count
1033
+ revenuecat.checkoutStarted(pkg, { source: variant }); // package, product, price, currency
1034
+
1035
+ const { customerInfo } = await Purchases.purchasePackage(pkg);
1036
+ if (revenuecat.purchaseCompleted(customerInfo, pkg, { source: variant })) navigateOn();
1037
+
1038
+ revenuecat.purchaseFailed(error, pkg, { source: variant }); // reason: "cancelled" | "error"
1039
+ revenuecat.purchasesRestored(await Purchases.restorePurchases());
1040
+ revenuecat.syncPlanTier(await Purchases.getCustomerInfo()); // at launch, no event
1041
+ ```
1042
+
1043
+ **The names are canonical**, the same way `WIRE_ONBOARDING_EVENTS` standardizes the onboarding funnel: `wire_paywall_shown`, `wire_checkout_started`, `wire_purchase_completed`, `wire_purchase_failed`, `wire_purchase_restored`. They are `app_event` `question_key` values on the wire, so they are also the exact strings a review or questionnaire firing trigger matches on.
1044
+
1045
+ **What the bridge decides for you.** The entitlement check lives in one place instead of being copy-pasted at the purchase site and the restore site. `plan_tier` (`paid` / `trial` / `free`) is written to the bound user context on every entitlement change, so the whole funnel can be sliced paid versus free. The store's error `message` is dropped and only the stable RevenueCat `code` rides the event, because that message is localized, unbounded, and occasionally names the account it failed for. And a purchase the store confirmed that granted **no** entitlement fires `wire_purchase_failed` with `reason: "not_entitled"` rather than returning quietly, which is the usual shape of a broken product-to-entitlement mapping.
1046
+
1047
+ ### The join key: `device_key`, never `session_id`
1048
+
1049
+ A purchase event is worth nothing unless you can join it to the same user's onboarding. Get this wrong and you do not get a wrong number, you get a permanent zero, which is far harder to notice.
1050
+
1051
+ `session_id` is not that key. The onboarding session id is the A2A `contextId`, the app-event session id gets minted per app-open, and the kit keeps those two id spaces apart on purpose. Intersect them and you get no rows. Ever.
1052
+
1053
+ The key is **`user_context.device_key`**, and you have to put it on both sides:
1054
+
1055
+ ```tsx
1056
+ import { activationJoinContext } from "@wireai/activation";
1057
+
1058
+ // Purchase side: createAnalytics / createWireActivation auto-mint and persist a device_key
1059
+ // and stamp it on every event. Supply your own to override it.
1060
+ const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userContext: { deviceKey } });
1061
+
1062
+ // Onboarding side: forward the SAME key. The kit passes userContext verbatim into the A2A
1063
+ // session-start metadata, and the server records it on the session's session_started event.
1064
+ <WireOnboarding config={config} userContext={activationJoinContext(deviceKey)} onComplete={persist} />
1065
+ ```
1066
+
1067
+ `activationJoinContext(deviceKey)` exists because the wire key is `device_key` and the prop-facing name is `deviceKey`. Hand-writing `userContext={{ deviceKey }}` produces a bucket the server's device lookup does not read, and you get the silent-zero funnel instead of an error. This is the one place that spelling is decided.
1068
+
1069
+ Skip the onboarding half and the purchase events are still valid on their own, they just cannot be attributed back to an onboarding.
1070
+
1071
+ **Since 0.12.2 the kit fills the gap for you.** If `<WireOnboarding>` gets `storage` and a `userContext` with no `device_key`, it injects its own per-install key: the SAME id `createAnalytics` / `createWireActivation` mint and persist, read back from storage rather than minted fresh. So the default wiring joins, and the code block above is now about *overriding* the key rather than about remembering it.
1072
+
1073
+ Three rules govern it:
1074
+
1075
+ - **Your key always wins.** Any non-empty `device_key` you pass is forwarded verbatim and nothing is merged over it. Existing integrations see a byte-identical payload.
1076
+ - **It needs `storage`.** Without persistence the kit's id is minted fresh on every launch, and a per-launch key is worse than no key: the server counts `min_sessions` by distinct app-opens grouped on `device_key`, so it corrupts the counter instead of leaving the join empty. With no `storage` the kit declines to inject and warns in dev.
1077
+ - **You can opt out.** `autoJoinKey={false}` restores the pre-0.12.2 behavior for a host that genuinely wants an unlinked session. The dev warning fires again there, naming the opt-out as the reason nothing was injected.
1078
+
968
1079
  ## Feature controls (per-module kill switches)
969
1080
 
970
1081
  Every activation surface has a switch you can flip from the dashboard "in case of something":
@@ -1,6 +1,6 @@
1
- export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-BGW9uXZJ.mjs';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-C0_odnIW.mjs';
3
- export { A as AnalyticsEvent, C as ClearUserContextOptions, a as ClientEvent, b as ClientEventTarget, c as ClientEventType, d as ContextEnvelope, e as ContextEnvelopeInput, f as EnvelopeSource, g as EventQueue, h as WIRE_ONBOARDING_EVENTS, i as WireOnboardingEventName, j as analyticsUserIdStorageKey, k as buildContextEnvelope, l as clearPiiFromContext, m as clearUserContext, n as createEventQueue, o as getCurrentSessionId, p as looksLikeEmail, q as makeSessionId, r as reportClientEvent, s as reportClientEventAwait, t as reportClientEvents, u as reportClientEventsAwait, v as resetCurrentSessionId, w as setCurrentSessionId, x as toAnalyticsEvent } from '../currentSession-C0_odnIW.mjs';
1
+ export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-CF_eHwzC.mjs';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-BlCeDP0f.mjs';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resolveAutoDeviceKey, F as setCurrentSessionId, G as toAnalyticsEvent } from '../currentSession-BlCeDP0f.mjs';
4
4
  import '../types-CNUqMK0D.mjs';
5
5
  import '../types-BKfpdZzX.mjs';
6
6
  import '../types-BcmagF6K.mjs';
@@ -33,7 +33,12 @@ interface ScreenTrackerOptions {
33
33
  serverUrl: string;
34
34
  apiKey: string;
35
35
  };
36
- /** The onboarding/session id to correlate screen views with, when known. */
36
+ /**
37
+ * The onboarding/session id to correlate screen views with, when known. Omitting it no longer
38
+ * means the view goes out WITHOUT a `session_id` (the server requires one and drops the event
39
+ * behind an HTTP 200 — that is why screen tracking silently produced nothing for a host that
40
+ * never mounted the lifecycle hook). `reportAppEvent` falls back to the current per-open id.
41
+ */
37
42
  sessionId?: string;
38
43
  /** A stable, non-PII device id — groups a device's sessions server-side. */
39
44
  deviceKey?: string;
@@ -103,8 +108,15 @@ type CreateAnalyticsConfig = {
103
108
  /** Tenant API key; sent as `Authorization: Bearer`. */
104
109
  apiKey: string;
105
110
  /**
106
- * Correlation id shared by every event from this instance (and the `identify` event). Defaults
107
- * to a fresh `makeSessionId()` at creation so all events agree on one id per analytics instance.
111
+ * ⚠️ OPT-OUT KNOB, not a default. Supplying a `sessionId` FREEZES the correlation id: every event
112
+ * this instance ever sends (including `identify`) is pinned to that one id, and the instance stops
113
+ * following the LIVE per-open session the server registered via `app.session_started`. Lifecycle
114
+ * analytics then collapse onto a single device-scoped session — one "first open", forever.
115
+ *
116
+ * OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and
117
+ * when no open has been registered yet it mints one AND registers it, so every later surface joins
118
+ * the same session instead of each inventing its own. Pass one ONLY if your host runs its own
119
+ * session lifecycle and owns the id the server should correlate on.
108
120
  */
109
121
  sessionId?: string;
110
122
  /** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */
@@ -1,6 +1,6 @@
1
- export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-jUJd5kxu.js';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-DdnUq2HQ.js';
3
- export { A as AnalyticsEvent, C as ClearUserContextOptions, a as ClientEvent, b as ClientEventTarget, c as ClientEventType, d as ContextEnvelope, e as ContextEnvelopeInput, f as EnvelopeSource, g as EventQueue, h as WIRE_ONBOARDING_EVENTS, i as WireOnboardingEventName, j as analyticsUserIdStorageKey, k as buildContextEnvelope, l as clearPiiFromContext, m as clearUserContext, n as createEventQueue, o as getCurrentSessionId, p as looksLikeEmail, q as makeSessionId, r as reportClientEvent, s as reportClientEventAwait, t as reportClientEvents, u as reportClientEventsAwait, v as resetCurrentSessionId, w as setCurrentSessionId, x as toAnalyticsEvent } from '../currentSession-DdnUq2HQ.js';
1
+ export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DsRe4epC.js';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-BxEB37xt.js';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resolveAutoDeviceKey, F as setCurrentSessionId, G as toAnalyticsEvent } from '../currentSession-BxEB37xt.js';
4
4
  import '../types-Buj9Lw9t.js';
5
5
  import '../types-BKfpdZzX.js';
6
6
  import '../types-BcmagF6K.js';
@@ -33,7 +33,12 @@ interface ScreenTrackerOptions {
33
33
  serverUrl: string;
34
34
  apiKey: string;
35
35
  };
36
- /** The onboarding/session id to correlate screen views with, when known. */
36
+ /**
37
+ * The onboarding/session id to correlate screen views with, when known. Omitting it no longer
38
+ * means the view goes out WITHOUT a `session_id` (the server requires one and drops the event
39
+ * behind an HTTP 200 — that is why screen tracking silently produced nothing for a host that
40
+ * never mounted the lifecycle hook). `reportAppEvent` falls back to the current per-open id.
41
+ */
37
42
  sessionId?: string;
38
43
  /** A stable, non-PII device id — groups a device's sessions server-side. */
39
44
  deviceKey?: string;
@@ -103,8 +108,15 @@ type CreateAnalyticsConfig = {
103
108
  /** Tenant API key; sent as `Authorization: Bearer`. */
104
109
  apiKey: string;
105
110
  /**
106
- * Correlation id shared by every event from this instance (and the `identify` event). Defaults
107
- * to a fresh `makeSessionId()` at creation so all events agree on one id per analytics instance.
111
+ * ⚠️ OPT-OUT KNOB, not a default. Supplying a `sessionId` FREEZES the correlation id: every event
112
+ * this instance ever sends (including `identify`) is pinned to that one id, and the instance stops
113
+ * following the LIVE per-open session the server registered via `app.session_started`. Lifecycle
114
+ * analytics then collapse onto a single device-scoped session — one "first open", forever.
115
+ *
116
+ * OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and
117
+ * when no open has been registered yet it mints one AND registers it, so every later surface joins
118
+ * the same session instead of each inventing its own. Pass one ONLY if your host runs its own
119
+ * session lifecycle and owns the id the server should correlate on.
108
120
  */
109
121
  sessionId?: string;
110
122
  /** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */