@wireai/activation 0.13.6 → 0.14.0
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.
- package/AGENTS.md +21 -9
- package/CHANGELOG.md +172 -1
- package/INTEGRATION_PROMPT.md +7 -4
- package/README.md +10 -2
- package/dist/analytics/index.d.mts +9 -2
- package/dist/analytics/index.d.ts +9 -2
- package/dist/analytics/index.js +56 -12
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +56 -12
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{currentSession-DngW-QoD.d.mts → currentSession-CUvTOchb.d.mts} +35 -6
- package/dist/{currentSession-C5976akx.d.ts → currentSession-CW_5Mq4O.d.ts} +35 -6
- package/dist/index.d.mts +24 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +631 -548
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +631 -548
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs.map +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/OnboardingFlow.tsx +4 -3
- package/src/WireOnboarding.tsx +4 -3
- package/src/activation/useWireActivation.ts +14 -1
- package/src/activation/wireActivation.ts +68 -2
- package/src/analytics/analyticsFacade.ts +24 -0
- package/src/analytics/eventQueue.ts +106 -11
- package/src/analytics/reportClientEvent.ts +31 -7
- package/src/analytics/useAnalytics.ts +17 -0
- package/src/context/deviceId.ts +10 -3
- package/src/permissions/permissionMemory.ts +12 -1
- package/src/session/persistedSession.ts +32 -8
- package/src/session-analytics/lifecycle.ts +26 -5
- package/src/session-analytics/reportSessionStart.ts +14 -10
- package/src/session-analytics/useLifecycleEvents.ts +70 -32
- package/src/session-analytics/useSessionStart.ts +57 -15
- package/src/types.ts +16 -6
- package/src/utils/readPlan.ts +8 -5
package/AGENTS.md
CHANGED
|
@@ -51,7 +51,7 @@ subpaths are optional secondary feature modules; import one only if you use it.
|
|
|
51
51
|
- `WIRE_PERMISSION_EVENTS`, `resolvePermissionCopy(...)`: the canonical permission-funnel names and the shipped rationale copy behind `permissionScreens` (see the prop below). The kit imports no native permission module; the host injects `request`.
|
|
52
52
|
- `createRevenueCatBridge({ analytics, entitlementId })`: the RevenueCat purchase funnel (see "RevenueCat" below).
|
|
53
53
|
- `activationJoinContext(deviceKey)`: builds the `userContext` value that joins an onboarding session to the app's later events. Every `<WireOnboarding>` needs it (see step 7).
|
|
54
|
-
- `resolveAutoDeviceKey({ appId, storage })`: the kit's own
|
|
54
|
+
- `resolveAutoDeviceKey({ appId, storage })`: the kit's own per-install `device_key`, read synchronously. Use it for a stamp on an ordinary event, never as the cross-launch join key on `<WireOnboarding>` (it cannot tell you whether the id persists, see the join-key section). `hydrateDeviceIdentity({ appId, storage })` is the awaited, durability-reporting sibling.
|
|
55
55
|
- `useLifecycleEvents(config, options)`: the app-root lifecycle hook (see step 8). `useSessionStart` / `reportSessionStart` are the counter-owning alternatives.
|
|
56
56
|
- Types: `OnboardingTheme`, `OnboardingResult`, `OnboardingEvent`, `WireOnboardingConfig`, `WireOnboardingProps`, `StepValidator`, `OnboardingCopy`, `IllustrationRegistry`.
|
|
57
57
|
|
|
@@ -60,7 +60,7 @@ subpaths are optional secondary feature modules; import one only if you use it.
|
|
|
60
60
|
| Prop | Type | Required | Notes |
|
|
61
61
|
|---|---|---|---|
|
|
62
62
|
| `config` | `WireOnboardingConfig` | yes | A2A transport + tenant. See the `config` fields table below. |
|
|
63
|
-
| `onComplete` | `(result: OnboardingResult) => void` | yes | Terminal recap CTA tapped. Persist `answers` via your profile-update path, then navigate on. `result` always has `answers` + `raw`; since 0.13.6 it also carries `plan` (the backend's onboarding plan,
|
|
63
|
+
| `onComplete` | `(result: OnboardingResult) => void` | yes | Terminal recap CTA tapped. Persist `answers` via your profile-update path, then navigate on. `result` always has `answers` + `raw`; since 0.13.6 it also carries `plan` (the backend's onboarding plan, whenever the backend sends one; the kit never infers it from the flow type) and `variant` (the assigned experiment arm) when the backend sent them. Both keys are ABSENT otherwise, so an integration written before 0.13.6 sees no change. The kit does not interpret either one: validate `plan` before applying it. |
|
|
64
64
|
| `theme` | `Partial<OnboardingTheme>` | no | Colors + font family names + radius/spacing, deep-merged over a neutral default. For dark/light, pass a different theme per app theme-state. |
|
|
65
65
|
| `components` | `WireAIComponent[]` | no | Override registered cards (default `onboardingComponents`). |
|
|
66
66
|
| `illustrations` | `Record<string, ReactNode>` | no | Host artwork for `InterstitialCard`, keyed by name. `{ ...defaultIllustrations, ...myArt }`. |
|
|
@@ -112,8 +112,8 @@ FACADE (`createAnalytics` and friends) is a different surface and lives on `@wir
|
|
|
112
112
|
4. **Config**: set `EXPO_PUBLIC_WIREAI_API_KEY` / `_SERVER_URL` / `_APP_ID`; build via `wireConfigFromEnv({ appId })`.
|
|
113
113
|
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.
|
|
114
114
|
6. **Theme**: `themeFromBrand({ primary })` or a full `Partial<OnboardingTheme>`; for dark/light, pick the theme by the app's theme state.
|
|
115
|
-
7. **Render + continuation**: drop `<WireOnboarding>` into the signup flow; `onComplete` → persist + navigate, `onSkip` → navigate. Pass the join key: `userContext={activationJoinContext(deviceKey)}`, or
|
|
116
|
-
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
|
|
115
|
+
7. **Render + continuation**: drop `<WireOnboarding>` into the signup flow; `onComplete` → persist + navigate, `onSkip` → navigate. Pass the join key: `userContext={activationJoinContext(deviceKey)}`, or, when the app owns no device id, pass a working `storage` and leave `userContext` alone so the kit injects its own (never hand-build it from `resolveAutoDeviceKey`, see the join-key section). Skipping both leaves the `activated` funnel permanently empty and reports no error.
|
|
116
|
+
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. That last part needs storage that actually persists: since 0.14.0 an adapter that throws or rejects is treated exactly like no storage, so the events fire with no auto key rather than a per-launch one that corrupts the count. `useSessionStart` / `reportSessionStart` are the alternatives for a host that already owns an open counter; neither emits `first_open`, and since 0.14.0 `useSessionStart` follows the same auto-key rule.
|
|
117
117
|
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.
|
|
118
118
|
|
|
119
119
|
## Review firing (do not hand-roll the decision fetch)
|
|
@@ -174,17 +174,29 @@ and activation surfaces auto-mint and stamp it on every event, and onboarding ge
|
|
|
174
174
|
`userContext={{ deviceKey }}`, because the server's device lookup reads `device_key` and a
|
|
175
175
|
misspelled bucket produces an empty funnel instead of an error.
|
|
176
176
|
|
|
177
|
-
**If the host owns no device id,
|
|
178
|
-
|
|
179
|
-
`createWireActivation` auto-mint and persist for this install, so both halves of
|
|
177
|
+
**If the host owns no device id, let the kit supply it.** Do not leave the onboarding side blank and
|
|
178
|
+
do not build the key yourself. Pass a working `storage` and the kit injects the exact id
|
|
179
|
+
`createAnalytics` / `createWireActivation` auto-mint and persist for this install, so both halves of
|
|
180
|
+
the join agree:
|
|
180
181
|
|
|
181
182
|
```tsx
|
|
182
|
-
import {
|
|
183
|
+
import { activationJoinContext } from "@wireai/activation";
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
// The app owns a device id: pass it.
|
|
185
186
|
<WireOnboarding config={config} userContext={activationJoinContext(deviceKey)} ... />
|
|
187
|
+
|
|
188
|
+
// The app owns none: pass `storage` and leave `userContext` alone. The kit injects the same id the
|
|
189
|
+
// analytics side stamps, after awaiting the storage read AND confirming the id actually persists.
|
|
190
|
+
<WireOnboarding config={{ ...config, storage }} ... />
|
|
186
191
|
```
|
|
187
192
|
|
|
193
|
+
⛔ Do NOT hand-build that key with `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`.
|
|
194
|
+
`resolveAutoDeviceKey` is synchronous by contract, so it returns a freshly minted id and adopts the
|
|
195
|
+
persisted one a storage read later, and it cannot report whether the id survives the launch at all.
|
|
196
|
+
Since 0.14.0 every kit surface that puts a device key on the wire as a cross-launch join awaits
|
|
197
|
+
`hydrateDeviceIdentity` and refuses a non-durable record; a host that really wants the value in hand
|
|
198
|
+
should do the same rather than the sync call.
|
|
199
|
+
|
|
188
200
|
Always pass `storage`. Without it the id is per-LAUNCH, not per-install, and a per-launch key makes
|
|
189
201
|
every open look like a new device, which breaks `min_sessions` and A/B arm stickiness as surely as
|
|
190
202
|
no key breaks the join. The same rule governs the lifecycle wiring: `useLifecycleEvents` falls back
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,175 @@
|
|
|
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
|
+
## [0.14.0] - 2026-08-17
|
|
7
|
+
|
|
8
|
+
A durability release, in two rounds. Round 1 stopped the two counted lifecycle events from riding a
|
|
9
|
+
per-launch device key. Round 2 came out of a fresh-eyes audit of the whole package under a
|
|
10
|
+
reproduction-first rule: eight findings, seven reproduced with a failing test before anything was
|
|
11
|
+
changed, one reproduced and deliberately left unfixed because its fix is a design ruling.
|
|
12
|
+
|
|
13
|
+
**MINOR, not patch, and only because of new API.** Every behaviour change here is a bug fix; the
|
|
14
|
+
version moved because four public surfaces gained members: `EventQueue.dispose()`,
|
|
15
|
+
`Analytics.dispose()`, `WireActivation.dispose()` and `WireActivationConfig.sink`. Nothing was
|
|
16
|
+
removed, nothing was renamed, and no existing call signature changed.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The event queue has a teardown, and needs one.** A queue claimed a persisted storage slot and
|
|
21
|
+
armed a backoff timer, and nothing ever gave either back — "already claimed" was read as "held by
|
|
22
|
+
a LIVE one", which is liveness nothing tracked. Two consequences, both silent. A remount (nav,
|
|
23
|
+
Fast Refresh, StrictMode's double-invoke, an `appId` that arrives async) rotated the replacement
|
|
24
|
+
onto `…#2`, so the whole launch persisted into a slot the NEXT launch never reads, and the `#N`
|
|
25
|
+
keys grew without bound. Worse, on the explicit-key path both queues address the SAME slot: the
|
|
26
|
+
dead one's retry timer woke up, drained, emptied, and `removeItem`d the live queue's persisted
|
|
27
|
+
`app.session_started`. (`unrefTimer` is a Node affordance; under Hermes that timer is fully
|
|
28
|
+
alive.) `dispose()` clears the timer, releases the claim and makes the queue inert;
|
|
29
|
+
`useLifecycleEvents`, `useAnalytics` and `useWireActivation` all call it on unmount and before
|
|
30
|
+
replacing an instance.
|
|
31
|
+
|
|
32
|
+
- **A storage read that TIMED OUT is no longer read as "nothing stored".** `withTimeout` resolved
|
|
33
|
+
`undefined` at the ceiling, which is byte-identical to a genuine empty read, and two callers then
|
|
34
|
+
did something irreversible with the guess. `loadPersistedSession` minted a new session id and
|
|
35
|
+
wrote it OVER the still-valid one it had not managed to read (the server then sees a second
|
|
36
|
+
`session_started` and back-fills the first as a phantom drop, the exact inflation that module
|
|
37
|
+
exists to prevent). `reportFirstOpen` treated the unread flag as "never fired" and emitted
|
|
38
|
+
`app.first_open` again on an install that already had one. Both now carry the distinct
|
|
39
|
+
`READ_TIMED_OUT` sentinel the event queue already had, and do nothing destructive on a timeout.
|
|
40
|
+
|
|
41
|
+
The same rule now covers an UNREADABLE flag (a locked / full / permission-denied adapter): that
|
|
42
|
+
fails on every launch, so emitting made `app.first_open` fire on every launch of that install,
|
|
43
|
+
counting one device as an unbounded number of installs. The kit's standing rule for an
|
|
44
|
+
unverifiable persistence answer, set in 0.13.0 and re-applied throughout this release, is to
|
|
45
|
+
decline rather than emit something that corrupts a metric. A host that wants `first_open` on a
|
|
46
|
+
device with no working storage should pass no `storage` at all, which is the documented degraded
|
|
47
|
+
mode and still fires once per process.
|
|
48
|
+
|
|
49
|
+
- **`Analytics.reset()` can no longer be undone by the read that was already in flight.** The
|
|
50
|
+
construction-time user-id hydration adopted its answer under `if (saved && !boundUserId)`, and a
|
|
51
|
+
logout is the one case where an empty binding is deliberate — so a read landing after `reset()`
|
|
52
|
+
re-bound the user who had just logged out, onto every subsequent event. `reset()` now marks the
|
|
53
|
+
hydration superseded.
|
|
54
|
+
|
|
55
|
+
- **The offline size cap no longer spends the funnel-denominator events first.** The buffer is
|
|
56
|
+
drop-oldest at 200, and at an app-open the oldest events in it are exactly `app.first_open` and
|
|
57
|
+
`app.session_started`. A host on the documented shared-sink wiring that went offline for a long
|
|
58
|
+
session pushed past the cap on screen views alone and evicted the open itself. Eviction now spends
|
|
59
|
+
ORDINARY events first; the cap stays a hard ceiling, so a buffer of nothing but `app.*` events
|
|
60
|
+
still evicts oldest-first rather than growing.
|
|
61
|
+
|
|
62
|
+
- **`wire.track()` no longer throws the event away when the POST fails.** It is the ACTION reporter,
|
|
63
|
+
called at exactly the moments a phone is offline, and a failed POST used to be the end of that
|
|
64
|
+
event's life — so the server-side trigger keyed on that `question_key` simply never fired for that
|
|
65
|
+
user. A failed send now goes to a durable buffer: your queue if you pass the new `sink`, otherwise
|
|
66
|
+
one this instance opens on the FIRST failure and never before. `track` still resolves `false` and
|
|
67
|
+
still does not revalidate, because the caller asked whether the server has it now.
|
|
68
|
+
|
|
69
|
+
⛔ Only a TRANSPORT failure is buffered. An event the server READ and refused (HTTP 200 carrying
|
|
70
|
+
`skipped`) is not retried: a retry is a re-decline, and it would park a poison event in a
|
|
71
|
+
persisted backlog that fails on every drain and every launch. `reportClientEventsOutcome` is the
|
|
72
|
+
internal seam that tells the two apart.
|
|
73
|
+
|
|
74
|
+
- **The unread-backlog guard is armed at construction, not 1500ms later.** It was only set in the
|
|
75
|
+
timeout branch, so for the first 1500ms of every launch the queue wrote freely over a blob it had
|
|
76
|
+
not read, and a drain that emptied in that window `removeItem`d it outright. An adapter that
|
|
77
|
+
resolves a read against its current state then answers `null` and the backlog is gone. The comment
|
|
78
|
+
claiming writes were suppressed "until the read lands" is now true.
|
|
79
|
+
|
|
80
|
+
- **`reportSessionStart`'s direct-POST fallback goes through `buildEventsRequest`.** It hand-built
|
|
81
|
+
its own url, headers and body, so it was the one send path of five that skipped the epoch-ms →
|
|
82
|
+
ISO8601 `ts` conversion — on the event `min_sessions` is counted from, where the failure shape is
|
|
83
|
+
HTTP 200 with `{written: 0, skipped: N}` and no client-visible error. Harmless today (that event
|
|
84
|
+
carries no `ts`), latent tomorrow. The "single choke point" claim in `reportClientEvent.ts` is now
|
|
85
|
+
held to the code by a test rather than by prose.
|
|
86
|
+
|
|
87
|
+
### Known, reproduced, NOT fixed
|
|
88
|
+
|
|
89
|
+
- **The review/questionnaire gates count PROCESSES, while an app-open is a 30-minute foreground.**
|
|
90
|
+
`currentOpenId()` is pinned for the life of the JS process. On iOS an app is suspended, not
|
|
91
|
+
killed, so a user opening the app daily for a week produces seven `app.session_started` events
|
|
92
|
+
(the SERVER's `min_sessions` advances to 7) while the on-device `wire_review_<id>_sessions` stays
|
|
93
|
+
at 1 forever, and a LOCAL `minSessions: 2` rule can never be met on a phone that is never
|
|
94
|
+
force-quit. It fails in the safe direction (a prompt that never shows), which is why it is not
|
|
95
|
+
being rushed.
|
|
96
|
+
|
|
97
|
+
Reproduced in `src/reviews/sessionCountAcrossOpens.test.ts`, marked `todo`, with the smallest
|
|
98
|
+
coherent fix written out next to it. It is deferred because that fix requires re-declaring the
|
|
99
|
+
`processOpenId` global slot LIVE and rewriting the `globalSlotDiscipline` canary exercise that
|
|
100
|
+
currently asserts this behaviour is correct — a canary written to guard the 2026-07-16 one-star
|
|
101
|
+
incident. Re-ruling it is a design decision, not an implementation detail.
|
|
102
|
+
|
|
103
|
+
- **`loadSettledPermissions` has the same timed-out-read class**, and its caller then persists a set
|
|
104
|
+
grown from `[]`, which can shrink the stored one. Made explicit in code rather than left to an
|
|
105
|
+
accident, but not changed: the fix needs a UX ruling (on an unknown set, does a resumed flow
|
|
106
|
+
re-ask a permission or skip it?), and guessing at that is how a user gets asked twice.
|
|
107
|
+
|
|
108
|
+
### Changed (docs)
|
|
109
|
+
|
|
110
|
+
- Every host-facing surface stopped recommending the defect class this release fixes. The README
|
|
111
|
+
prop table, `AGENTS.md`, `llms.txt`, `INTEGRATION_PROMPT.md`, the `WireOnboardingProps.userContext`
|
|
112
|
+
docstring and the `__DEV__` missing-join-key warning all told a host with no device id of its own
|
|
113
|
+
to write `userContext={activationJoinContext(resolveAutoDeviceKey({ appId, storage }))}`. That is
|
|
114
|
+
the synchronous, durability-blind read, used as a cross-launch join key. They now say to pass a
|
|
115
|
+
working `storage` and let the kit inject the key it has confirmed persists.
|
|
116
|
+
- `ai_rules/context_map.md` gained the four module rows it was missing (`src/analytics/`,
|
|
117
|
+
`src/session-analytics/`, `src/context/`, `src/device/`). The map is the file the repo's own rules
|
|
118
|
+
tell an agent to read instead of grepping `src/`, and it was silent on the directories this whole
|
|
119
|
+
release lives in.
|
|
120
|
+
|
|
121
|
+
## [0.13.7] — superseded by 0.14.0, never published
|
|
122
|
+
|
|
123
|
+
The round-1 device-key fixes below shipped as part of **0.14.0**. The entry is kept because it is the
|
|
124
|
+
detailed record of that half of the work.
|
|
125
|
+
|
|
126
|
+
The two events the server counts the funnel DENOMINATOR from stopped riding a per-launch device key.
|
|
127
|
+
`app.first_open` and `app.session_started` are the only events `min_sessions` is computed from (the
|
|
128
|
+
server counts distinct opens grouped by `user_context.device_key`), so a key that differs on every
|
|
129
|
+
launch does not merely leave that counter empty, it caps it at 1 and inflates distinct-device counts
|
|
130
|
+
at the same time. Both lifecycle hooks were doing exactly that, in two different ways.
|
|
131
|
+
|
|
132
|
+
Nothing on the wire changed shape, no export was added or removed, and a host-supplied `deviceKey` is
|
|
133
|
+
byte-identical to 0.13.6 on every path.
|
|
134
|
+
|
|
135
|
+
### Fixed
|
|
136
|
+
|
|
137
|
+
- **`useLifecycleEvents(...)` no longer stamps the in-memory mint when the storage read settles
|
|
138
|
+
NON-DURABLE.** It already awaited hydration before the mount fire, so a healthy store was correct.
|
|
139
|
+
A locked / full / permission-denied adapter is the case it missed: the read settles degraded, the
|
|
140
|
+
`.then` fired anyway, and the synchronous auto id went onto both counted events. Measured on
|
|
141
|
+
0.13.6: two launches over a throwing MMKV produced four distinct `wdev_*`.
|
|
142
|
+
|
|
143
|
+
The auto path now resolves through `hydrateDeviceIdentity(...)`, the provenance-carrying read, and
|
|
144
|
+
refuses a `durable: false` outcome, which is the rule `src/context/deviceId.ts` has stated for
|
|
145
|
+
these callers since 0.13.0 and `<WireOnboarding>`'s `autoJoinKey` has obeyed since then. A refusal
|
|
146
|
+
emits both events with NO auto `device_key`, the same branch a config with no `storage` already
|
|
147
|
+
took, never a silenced event. **Every fire path obeys it**, not just the mount one: the foreground
|
|
148
|
+
re-fire after a real background re-runs the same read, so a transient cold-boot storage lock gets a
|
|
149
|
+
fresh attempt on the next open instead of a verdict cached for the process lifetime.
|
|
150
|
+
|
|
151
|
+
- **`useSessionStart(...)` waits for the persisted key before its mount fire.** This hook never got
|
|
152
|
+
the 0.13.3 hydration fix its sibling did, so it stamped a brand-new `wdev_*` on `app.session_started`
|
|
153
|
+
on every launch even on a perfectly HEALTHY store, while every other kit surface adopted the
|
|
154
|
+
persisted id milliseconds later. Any host wiring this path had a structurally unsatisfiable
|
|
155
|
+
`min_sessions` and no error anywhere. It now takes the same awaited, durability-checked auto path as
|
|
156
|
+
`useLifecycleEvents(...)`, on the mount fire and on the foreground re-fire.
|
|
157
|
+
|
|
158
|
+
It is not deprecated and its contract is otherwise unchanged: a host-supplied `deviceKey` and a
|
|
159
|
+
config with no `storage` both still fire synchronously, because there is nothing to read.
|
|
160
|
+
|
|
161
|
+
### Unchanged
|
|
162
|
+
|
|
163
|
+
- `createAnalytics(...)` / `createWireActivation(...)` keep stamping the synchronous
|
|
164
|
+
`resolveAutoDeviceKey(...)` per event, deliberately. Their documented contract is that they never
|
|
165
|
+
throw and degrade to the in-memory id, they are numerator paths rather than the counted opens, and
|
|
166
|
+
they re-read the registry per event so they adopt the persisted id as soon as it lands.
|
|
167
|
+
- `hydrateAutoDeviceKey(...)` stays exported and behaves identically. No kit surface uses it any more:
|
|
168
|
+
it returns a bare string, and a string cannot say whether it survives the launch, which is the whole
|
|
169
|
+
reason `hydrateDeviceIdentity(...)` exists.
|
|
170
|
+
- Docs swept for the claim this changes: the README lifecycle section, `AGENTS.md` step 8, `llms.txt`
|
|
171
|
+
and `INTEGRATION_PROMPT.md` all said "with `config.storage` it stamps the persisted auto
|
|
172
|
+
`device_key`". Presence of the prop was never the condition. It is now written as what it is: a
|
|
173
|
+
storage adapter that throws or rejects counts as no storage.
|
|
174
|
+
|
|
6
175
|
## [0.13.6] - 2026-08-09
|
|
7
176
|
|
|
8
177
|
Two things the backend already decides, handed to the host instead of guessed at. Both ride the
|
|
@@ -19,7 +188,9 @@ entry. This entry is that debt paid, plus the `variant` half, released together
|
|
|
19
188
|
On the AI path the server appends a SECOND A2A DataPart to the turn it finishes on,
|
|
20
189
|
`{ kind: "onboarding_plan", plan: {...} }`, alongside the component envelope the renderer already
|
|
21
190
|
consumes. A new pure reader, `src/utils/readPlan.ts`, lifts it off the thread and `handleFinish`
|
|
22
|
-
puts it on the result.
|
|
191
|
+
puts it on the result. A run the server finishes without a plan is unchanged: the key stays off
|
|
192
|
+
the result object entirely. Which runs carry a plan is the backend's configuration, and the
|
|
193
|
+
reader never infers it from the flow type.
|
|
23
194
|
|
|
24
195
|
The one thing the reader is allowed to reject is a structural fact: a plan is an object. A scalar,
|
|
25
196
|
an array, a misspelled marker or a missing payload yields `undefined` and the host falls back to
|
package/INTEGRATION_PROMPT.md
CHANGED
|
@@ -41,9 +41,10 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
41
41
|
dead-ends.
|
|
42
42
|
- Forward install attribution if I have it: `wireConfigFromEnv({ appId, metadata: attributionMetadata({...}) })`.
|
|
43
43
|
- THE JOIN KEY: pass `userContext={activationJoinContext(deviceKey)}`. If my app owns no stable
|
|
44
|
-
device id,
|
|
45
|
-
the
|
|
46
|
-
|
|
44
|
+
device id, pass a working `storage` and leave `userContext` alone: the kit injects the same id
|
|
45
|
+
the analytics side stamps, after confirming it actually persists. Do NOT hand-build it from
|
|
46
|
+
`resolveAutoDeviceKey` (synchronous, and it cannot report durability). Leaving both out makes
|
|
47
|
+
the `activated` funnel read zero forever and reports no error.
|
|
47
48
|
- Analytics: log via `toAnalyticsEvent(e)` → my logger, using the canonical `wire_onboarding_*` names.
|
|
48
49
|
7. Navigation: mount the screen in my first-run/signup flow; branch to the static flow when the gate
|
|
49
50
|
is off or config is missing.
|
|
@@ -51,7 +52,9 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
51
52
|
ONCE at my app root, above the navigator, before anything else touches analytics. Pass
|
|
52
53
|
`config.storage`. It is the only path that emits `app.first_open`, it registers the per-open
|
|
53
54
|
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
|
+
`device_key` on `app.session_started`, which is what the server counts `min_sessions` from. If my
|
|
56
|
+
storage adapter throws or rejects, the kit treats it as no storage and stamps no auto key at all
|
|
57
|
+
(0.14.0), because a key that changes every launch corrupts that count.
|
|
55
58
|
`useSessionStart` / `reportSessionStart` are only for a host that already owns an open counter;
|
|
56
59
|
neither emits `first_open`.
|
|
57
60
|
9. Event triggers (only if I asked to fire a review/questionnaire off an in-app action): pick the
|
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
231
231
|
| Prop | Type | Notes |
|
|
232
232
|
|---|---|---|
|
|
233
233
|
| `config` | `{ apiKey, serverUrl, appId, metadata?, appVersion? }` | A2A transport; the key resolves the tenant server-side. `appVersion` is host-injected (e.g. `Constants.expoConfig?.version`) and forwarded for analytics segmentation. **Required.** |
|
|
234
|
-
| `onComplete` | `(result: OnboardingResult) => void` | Fires when the user taps the terminal recap's CTA. `result` always carries `answers` and `raw`. Since 0.13.6 it may also carry `plan` (the backend's onboarding plan,
|
|
234
|
+
| `onComplete` | `(result: OnboardingResult) => void` | Fires when the user taps the terminal recap's CTA. `result` always carries `answers` and `raw`. Since 0.13.6 it may also carry `plan` (the backend's onboarding plan, present whenever the backend sends one; the kit never infers it from the flow type) and `variant` (the experiment arm the backend assigned, when the tenant runs one). Each of those two keys is set ONLY when the backend actually sent one, so a run without them returns exactly the object earlier versions returned: `"plan" in result` and `"variant" in result` both stay false. The kit does not interpret either: **validate `plan` before you apply it.** **Required.** |
|
|
235
235
|
| `theme` | `Partial<OnboardingTheme>` | Brand colors/fonts/radius/spacing, deep-merged over a neutral default. |
|
|
236
236
|
| `illustrations` | `Record<string, ReactNode>` | App artwork for `InterstitialCard`, keyed by name. |
|
|
237
237
|
| `icons` | `Record<string, ReactNode>` | Your own icon nodes, keyed by the semantic name the AI emits (`{ instagram: <BrandIg/> }`). Checked first, so use it to put your brand mark on a choice card, to add names the vocabulary does not carry, or to get icons at all without installing `@expo/vector-icons`. Anything you leave out falls back to that optional peer, then to no icon. Never a crash. See [Icons](#icons). |
|
|
@@ -251,7 +251,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
251
251
|
| `sessionTtlMs` | `number` | How long a persisted session id stays resumable. Default `3600000` (1h, the backend's session TTL). With `storage` only. |
|
|
252
252
|
| `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. |
|
|
253
253
|
| `retainSessionOnComplete` | `boolean` | Opt IN: keep the persisted session seed alive across completion. A multi-stage signup that re-enters onboarding (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. With `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). With `storage` only. |
|
|
254
|
-
| `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)}`
|
|
254
|
+
| `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)}`. If your app owns no device id, pass a working `storage` and leave this prop alone: the kit injects its own key, and only after it has confirmed the key actually persists. ⛔ Do not hand-build that key with `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`, because `resolveAutoDeviceKey` is synchronous by contract (it hands back a fresh mint and adopts the persisted id a storage read later) and it cannot tell you whether the id survives the launch at all. 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). |
|
|
255
255
|
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), 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)). |
|
|
256
256
|
| `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). |
|
|
257
257
|
|
|
@@ -421,6 +421,14 @@ none, and waits for that key to be read back before it fires, so the events the
|
|
|
421
421
|
`min_sessions` from carry the id this install keeps. Without storage the id would be per-launch, which
|
|
422
422
|
would make every open look like a new device, so the fallback deliberately does not engage.
|
|
423
423
|
|
|
424
|
+
Since 0.14.0 that also covers a `storage` adapter that is present but cannot persist (locked, full,
|
|
425
|
+
permission denied, so it throws or rejects). The read settles non-durable and the hook declines the
|
|
426
|
+
key rather than stamping the in-memory one. Both events still fire, they just carry no auto
|
|
427
|
+
`device_key`. That is the same verdict `autoJoinKey` reaches on `<WireOnboarding>`, for the same
|
|
428
|
+
reason: a key that changes on every launch corrupts `min_sessions` and inflates distinct-device
|
|
429
|
+
counts, where an absent key only leaves the join empty. A `deviceKey` you supply yourself is never
|
|
430
|
+
touched by any of this.
|
|
431
|
+
|
|
424
432
|
The two alternatives below are for a host that already owns an open counter. Both emit
|
|
425
433
|
`app.session_started` only, so `app.first_open` stays empty forever on those paths.
|
|
426
434
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-j5gFfJhK.mjs';
|
|
2
|
-
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, 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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-CUvTOchb.mjs';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, 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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-CUvTOchb.mjs';
|
|
4
4
|
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.mjs';
|
|
5
5
|
import '../types-Cju-1_jT.mjs';
|
|
6
6
|
import '../types-BKfpdZzX.mjs';
|
|
@@ -268,6 +268,13 @@ type Analytics = {
|
|
|
268
268
|
notifyOnline(): void;
|
|
269
269
|
/** Current pending (in-memory) count. */
|
|
270
270
|
size(): number;
|
|
271
|
+
/**
|
|
272
|
+
* Tear the instance's event queue down when its owner goes away (see {@link EventQueue.dispose}).
|
|
273
|
+
* Idempotent, never throws. Call it if you build an instance per screen / per mount: the queue
|
|
274
|
+
* claims a persisted storage slot, and a replacement built before the old one released it lands on
|
|
275
|
+
* a rotated `…#2` key that no later launch ever reads. `useAnalytics` does this for you.
|
|
276
|
+
*/
|
|
277
|
+
dispose(): void;
|
|
271
278
|
};
|
|
272
279
|
/**
|
|
273
280
|
* Create a bound analytics instance. Seeds one correlation `sessionId`, builds an offline-first
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-B_0SgCBe.js';
|
|
2
|
-
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, 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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-CW_5Mq4O.js';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, 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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-CW_5Mq4O.js';
|
|
4
4
|
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.js';
|
|
5
5
|
import '../types-h2BZvl1t.js';
|
|
6
6
|
import '../types-BKfpdZzX.js';
|
|
@@ -268,6 +268,13 @@ type Analytics = {
|
|
|
268
268
|
notifyOnline(): void;
|
|
269
269
|
/** Current pending (in-memory) count. */
|
|
270
270
|
size(): number;
|
|
271
|
+
/**
|
|
272
|
+
* Tear the instance's event queue down when its owner goes away (see {@link EventQueue.dispose}).
|
|
273
|
+
* Idempotent, never throws. Call it if you build an instance per screen / per mount: the queue
|
|
274
|
+
* claims a persisted storage slot, and a replacement built before the old one released it lands on
|
|
275
|
+
* a rotated `…#2` key that no later launch ever reads. `useAnalytics` does this for you.
|
|
276
|
+
*/
|
|
277
|
+
dispose(): void;
|
|
271
278
|
};
|
|
272
279
|
/**
|
|
273
280
|
* Create a bound analytics instance. Seeds one correlation `sessionId`, builds an offline-first
|
package/dist/analytics/index.js
CHANGED
|
@@ -76,20 +76,21 @@ var reportClientEvents = (target, events) => {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
|
|
79
|
-
var reportClientEventsAwait = async (target, events) =>
|
|
79
|
+
var reportClientEventsAwait = async (target, events) => await reportClientEventsOutcome(target, events) === "delivered";
|
|
80
|
+
var reportClientEventsOutcome = async (target, events) => {
|
|
80
81
|
try {
|
|
81
82
|
const req = buildEventsRequest(target, events);
|
|
82
|
-
if (!req) return
|
|
83
|
+
if (!req) return "refused";
|
|
83
84
|
const res = await fetch(req.url, req.init);
|
|
84
|
-
if (!res || !res.ok) return
|
|
85
|
+
if (!res || !res.ok) return "unreachable";
|
|
85
86
|
const ack = await readEventsAck(res);
|
|
86
|
-
if (!ack || ack.skipped <= 0) return
|
|
87
|
+
if (!ack || ack.skipped <= 0) return "delivered";
|
|
87
88
|
if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
|
|
88
89
|
console.warn(describeDiscarded(ack));
|
|
89
90
|
}
|
|
90
|
-
return
|
|
91
|
+
return "refused";
|
|
91
92
|
} catch {
|
|
92
|
-
return
|
|
93
|
+
return "unreachable";
|
|
93
94
|
}
|
|
94
95
|
};
|
|
95
96
|
var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
|
|
@@ -692,6 +693,9 @@ var claimQueueKey = (preferred, explicit) => {
|
|
|
692
693
|
);
|
|
693
694
|
return key;
|
|
694
695
|
};
|
|
696
|
+
var releaseQueueKey = (key) => {
|
|
697
|
+
claimedQueueKeys().delete(key);
|
|
698
|
+
};
|
|
695
699
|
var resetEventQueueKeys = () => claimedQueueKeys().clear();
|
|
696
700
|
var READ_TIMED_OUT = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
|
|
697
701
|
var withTimeout = (p, ms) => {
|
|
@@ -737,7 +741,8 @@ var createEventQueue = (options) => {
|
|
|
737
741
|
let flushing = false;
|
|
738
742
|
let attempt = 0;
|
|
739
743
|
let retryTimer;
|
|
740
|
-
let
|
|
744
|
+
let disposed = false;
|
|
745
|
+
let backlogUnread = storage !== void 0;
|
|
741
746
|
const resolveEnvelope = () => {
|
|
742
747
|
try {
|
|
743
748
|
return typeof options.envelope === "function" ? options.envelope() : options.envelope;
|
|
@@ -762,6 +767,7 @@ var createEventQueue = (options) => {
|
|
|
762
767
|
};
|
|
763
768
|
const persist = () => {
|
|
764
769
|
if (!storage) return;
|
|
770
|
+
if (disposed) return;
|
|
765
771
|
if (backlogUnread) return;
|
|
766
772
|
try {
|
|
767
773
|
if (pending.length === 0) {
|
|
@@ -775,8 +781,20 @@ var createEventQueue = (options) => {
|
|
|
775
781
|
} catch {
|
|
776
782
|
}
|
|
777
783
|
};
|
|
784
|
+
const isCountedOpenEvent = (event) => typeof event.question_key === "string" && event.question_key.startsWith("app.");
|
|
778
785
|
const enforceSizeCap = () => {
|
|
779
|
-
|
|
786
|
+
let overflow = pending.length - maxSize;
|
|
787
|
+
if (overflow <= 0) return;
|
|
788
|
+
const kept = [];
|
|
789
|
+
for (const item of pending) {
|
|
790
|
+
if (overflow > 0 && !isCountedOpenEvent(item.event)) {
|
|
791
|
+
overflow--;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
kept.push(item);
|
|
795
|
+
}
|
|
796
|
+
if (overflow > 0) kept.splice(0, overflow);
|
|
797
|
+
pending = kept;
|
|
780
798
|
};
|
|
781
799
|
const safeSig = (event) => {
|
|
782
800
|
try {
|
|
@@ -822,8 +840,12 @@ var createEventQueue = (options) => {
|
|
|
822
840
|
});
|
|
823
841
|
return;
|
|
824
842
|
}
|
|
843
|
+
backlogUnread = false;
|
|
825
844
|
mergePersisted(parsePersisted(raced));
|
|
845
|
+
persist();
|
|
826
846
|
} catch {
|
|
847
|
+
backlogUnread = false;
|
|
848
|
+
persist();
|
|
827
849
|
}
|
|
828
850
|
})();
|
|
829
851
|
const postBatch = async (events) => {
|
|
@@ -859,10 +881,12 @@ var createEventQueue = (options) => {
|
|
|
859
881
|
unrefTimer(retryTimer);
|
|
860
882
|
};
|
|
861
883
|
const drain = async () => {
|
|
884
|
+
if (disposed) return;
|
|
862
885
|
try {
|
|
863
886
|
await loadPromise;
|
|
864
887
|
} catch {
|
|
865
888
|
}
|
|
889
|
+
if (disposed) return;
|
|
866
890
|
if (flushing) return;
|
|
867
891
|
flushing = true;
|
|
868
892
|
try {
|
|
@@ -890,6 +914,7 @@ var createEventQueue = (options) => {
|
|
|
890
914
|
}
|
|
891
915
|
};
|
|
892
916
|
const enqueue = (event) => {
|
|
917
|
+
if (disposed) return;
|
|
893
918
|
try {
|
|
894
919
|
const stamped = stamp(event);
|
|
895
920
|
const sig = safeSig(stamped);
|
|
@@ -909,7 +934,13 @@ var createEventQueue = (options) => {
|
|
|
909
934
|
flush();
|
|
910
935
|
};
|
|
911
936
|
const size = () => pending.length;
|
|
912
|
-
|
|
937
|
+
const dispose = () => {
|
|
938
|
+
if (disposed) return;
|
|
939
|
+
disposed = true;
|
|
940
|
+
clearRetry();
|
|
941
|
+
if (storage) releaseQueueKey(key);
|
|
942
|
+
};
|
|
943
|
+
return { enqueue, flush, notifyOnline, size, dispose };
|
|
913
944
|
};
|
|
914
945
|
|
|
915
946
|
// src/identity/userIdentity.ts
|
|
@@ -1147,8 +1178,10 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1147
1178
|
});
|
|
1148
1179
|
let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
|
|
1149
1180
|
const storageKey = analyticsUserIdStorageKey(config.appId);
|
|
1181
|
+
let hydrationSuperseded = false;
|
|
1150
1182
|
if (config.storage) {
|
|
1151
1183
|
void config.storage.getItem(storageKey).then((saved) => {
|
|
1184
|
+
if (hydrationSuperseded) return;
|
|
1152
1185
|
if (saved && !boundUserId) boundUserId = saved;
|
|
1153
1186
|
}).catch(() => {
|
|
1154
1187
|
});
|
|
@@ -1190,6 +1223,7 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1190
1223
|
}
|
|
1191
1224
|
};
|
|
1192
1225
|
const reset = () => {
|
|
1226
|
+
hydrationSuperseded = true;
|
|
1193
1227
|
boundUserId = void 0;
|
|
1194
1228
|
userContext = clearPiiFromContext(userContext);
|
|
1195
1229
|
if (config.storage) void config.storage.removeItem(storageKey).catch(() => {
|
|
@@ -1247,23 +1281,33 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1247
1281
|
reset,
|
|
1248
1282
|
flush: queue.flush,
|
|
1249
1283
|
notifyOnline: queue.notifyOnline,
|
|
1250
|
-
size: queue.size
|
|
1284
|
+
size: queue.size,
|
|
1285
|
+
dispose: queue.dispose
|
|
1251
1286
|
};
|
|
1252
1287
|
};
|
|
1253
1288
|
var useAnalytics = (config, options = {}) => {
|
|
1254
|
-
var _a;
|
|
1289
|
+
var _a, _b;
|
|
1255
1290
|
const ref = react.useRef(void 0);
|
|
1256
1291
|
const prevKeys = react.useRef("");
|
|
1257
1292
|
const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
|
|
1258
1293
|
if (!ref.current || prevKeys.current !== currentKeys) {
|
|
1259
1294
|
prevKeys.current = currentKeys;
|
|
1295
|
+
(_a = ref.current) == null ? void 0 : _a.dispose();
|
|
1260
1296
|
ref.current = createAnalytics(config, options);
|
|
1261
1297
|
}
|
|
1262
|
-
const hostDeviceKey = typeof ((
|
|
1298
|
+
const hostDeviceKey = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
|
|
1263
1299
|
react.useEffect(() => {
|
|
1264
1300
|
var _a2;
|
|
1265
1301
|
if (hostDeviceKey) (_a2 = ref.current) == null ? void 0 : _a2.setUserContext({ deviceKey: hostDeviceKey });
|
|
1266
1302
|
}, [hostDeviceKey]);
|
|
1303
|
+
react.useEffect(
|
|
1304
|
+
() => () => {
|
|
1305
|
+
var _a2;
|
|
1306
|
+
(_a2 = ref.current) == null ? void 0 : _a2.dispose();
|
|
1307
|
+
ref.current = void 0;
|
|
1308
|
+
},
|
|
1309
|
+
[]
|
|
1310
|
+
);
|
|
1267
1311
|
return ref.current;
|
|
1268
1312
|
};
|
|
1269
1313
|
|