@wireai/activation 0.12.2 → 0.13.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/CHANGELOG.md +145 -0
- package/README.md +5 -3
- package/dist/analytics/index.d.mts +2 -2
- package/dist/analytics/index.d.ts +2 -2
- package/dist/analytics/index.js +114 -35
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +114 -36
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{currentSession-BxEB37xt.d.ts → currentSession-D7zabMXK.d.ts} +161 -9
- package/dist/{currentSession-BlCeDP0f.d.mts → currentSession-_GynvhzT.d.mts} +161 -9
- package/dist/index.d.mts +5 -15
- package/dist/index.d.ts +5 -15
- package/dist/index.js +232 -135
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -134
- 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 +8 -6
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +8 -6
- package/dist/reviews/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/OnboardingFlow.tsx +10 -3
- package/src/WireOnboarding.tsx +115 -32
- package/src/activation/wireActivation.ts +13 -7
- package/src/analytics/analyticsFacade.ts +11 -10
- package/src/analytics/currentSession.ts +6 -20
- package/src/analytics/eventQueue.ts +69 -1
- package/src/analytics/index.ts +1 -1
- package/src/analytics/reportClientEvent.ts +92 -29
- package/src/config/wireConfigFromEnv.ts +1 -10
- package/src/context/deviceId.ts +77 -16
- package/src/context/userContext.ts +4 -15
- package/src/identity/identityRecord.ts +123 -0
- package/src/identity/userIdentity.ts +45 -9
- package/src/index.ts +6 -4
- package/src/session-analytics/useLifecycleEvents.ts +10 -1
- package/src/types.ts +14 -0
- package/src/utils/deriveAnswers.ts +6 -2
- package/src/utils/readProgress.ts +4 -0
- package/src/utils/warnInDev.ts +33 -0
- package/src/components/DoneBlock.tsx +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,151 @@
|
|
|
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.13.0] — 2026-07-27
|
|
7
|
+
|
|
8
|
+
The identity-provenance release. Every id-layer defect fixed here is one omission wearing five faces:
|
|
9
|
+
`session_id` and `device_key` are bare `string`s minted independently by four subsystems, and nothing
|
|
10
|
+
anywhere recorded WHERE a given id came from. So a rejecting storage adapter's in-memory id was
|
|
11
|
+
indistinguishable from a persisted one, an app-OPEN session id could be posted into a field meaning the
|
|
12
|
+
ONBOARDING session, an auto-minted key could be injected beside a device id the host demonstrably owned
|
|
13
|
+
on another surface, and the join key could change identity mid-mount — each of them silently, each of
|
|
14
|
+
them returning success. This release gives ids provenance and teaches every consumer to refuse a
|
|
15
|
+
fabricated one.
|
|
16
|
+
|
|
17
|
+
Nothing here changes the wire for a correctly-wired host. The behaviour changes all move in one
|
|
18
|
+
direction: the kit now declines and says so, where it used to fabricate and return `true`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`resolveIdentity(...)`, `hostIdentity(...)` and the `IdentityRecord` type** (`identity/identityRecord.ts`) —
|
|
23
|
+
the substrate. One record, `{ value, space, source, durable }`, parked on the same `Symbol.for`
|
|
24
|
+
registry pattern the repo already uses for the current session id, the auto device key and activation
|
|
25
|
+
revalidation, so every inlined bundle copy addresses ONE registry. `space` is
|
|
26
|
+
`onboarding-session | app-session | device`, `source` is `host | auto`, and `durable` says whether the
|
|
27
|
+
value was actually persisted. A host-sourced record is recorded on a process-wide registry, which is
|
|
28
|
+
what lets one surface notice that another surface owns a device key it was not given.
|
|
29
|
+
⚠️ Deliberately NOT a branded-types refactor — `OnboardingSessionId` / `AppSessionId` / `DeviceKey`
|
|
30
|
+
across every signature is real value and is deferred, because it touches every file.
|
|
31
|
+
- **`hydrateDeviceIdentity(...)`** (`context/deviceId.ts`) — the provenance-carrying sibling of
|
|
32
|
+
`hydrateAutoDeviceKey`. Same awaited read, but it answers "is this an id this install will KEEP?"
|
|
33
|
+
instead of only "what is the id?". `hydrateAutoDeviceKey` is unchanged and still returns a bare string.
|
|
34
|
+
- **`OnboardingProgress.slot_id`** — an optional STABLE per-slot identity for a screen, read off
|
|
35
|
+
`props.progress` by `readProgress` and preferred over `key` by `deriveAnswers`. `key` is authored from
|
|
36
|
+
the question's prompt text (tenant flows slugify it and cut at 32 chars), so re-wording a question mints
|
|
37
|
+
a NEW key and a host reading `answers.interests` silently starts reading `undefined`.
|
|
38
|
+
⚠️ **INERT UNTIL THE SERVER EMITS IT.** No deployed server sends `slot_id` today. Against a server that
|
|
39
|
+
never sends it, every path degrades to byte-identical 0.12.2 behaviour. Every fallback is decided
|
|
40
|
+
PER CARD, never cached as a per-session "this backend supports slots" verdict, so a thread that mixes
|
|
41
|
+
slotted and unslotted cards — the real shape during a rollout — keys each card correctly.
|
|
42
|
+
- **`allowAppSessionFallback` on `identifyOnboarding`** — the opt-in switch for the tier-3 behaviour
|
|
43
|
+
described under Changed. Default `false`.
|
|
44
|
+
- **`resetEventQueueKeys()`** (`@wireai/activation/analytics`, test-only) — clears
|
|
45
|
+
the claimed-queue-key registry. A real relaunch is a new process; a test that simulates one in-process
|
|
46
|
+
needs this or its second queue reads as a concurrent sibling.
|
|
47
|
+
- **A first test file for `deriveAnswers`** (`utils/deriveAnswers.test.ts`), table-driven over real thread
|
|
48
|
+
shapes: progress key present and absent, slugified prose, multi-select, every NL wrapper the SDK
|
|
49
|
+
produces, and the threads that must produce NO entry.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **⚠️ BEHAVIOR CHANGE: a `storage` adapter that FAILS no longer earns an injected join key.** The
|
|
54
|
+
auto-join gate was a bare presence check on the `storage` prop — never the SUCCESS of the write — and
|
|
55
|
+
`hydrateAutoDeviceKey` resolves to the in-memory mint on every failure branch, so a REJECTING adapter
|
|
56
|
+
(a locked / full / permission-denied AsyncStorage, the most common real breakage) injected a FRESH
|
|
57
|
+
`wdev_*` on every launch. Probe-verified: two launches, two different keys. That is strictly worse than
|
|
58
|
+
injecting nothing — the server counts `min_sessions` by distinct opens grouped on `device_key`, so a
|
|
59
|
+
per-launch key makes that counter structurally incapable of exceeding 1 AND inflates distinct-device
|
|
60
|
+
counts. The gate now reads `IdentityRecord.durable`, true only when the id was adopted from storage or
|
|
61
|
+
written to it successfully, so a broken adapter lands on the same "declined, and here is why" path as
|
|
62
|
+
no adapter at all. The existing dev warning was widened to name this third reason.
|
|
63
|
+
Who this changes: a host whose storage was silently broken. It was already getting corrupt data; it now
|
|
64
|
+
gets no join key and a warning that names the cause.
|
|
65
|
+
- **⚠️ BEHAVIOR CHANGE: `identifyOnboarding` no longer binds to the app-open session by default, and its
|
|
66
|
+
return type widened to `"onboarding" | "app_session" | false`.** Tier 3 fell back to
|
|
67
|
+
`getCurrentSessionId()` — the per-OPEN session — and posted it in the `session_id` field, which on this
|
|
68
|
+
endpoint means the ONBOARDING session, then returned `true`. Two disjoint id spaces share that field, so
|
|
69
|
+
the row it wrote could never join the onboarding funnel while the host was told it had worked. It fired
|
|
70
|
+
in exactly the DOCUMENTED post-completion case, because completion clears the persisted session. The
|
|
71
|
+
fallback is now behind `allowAppSessionFallback` and, when it does fire, says so in the return value.
|
|
72
|
+
⚠️ `"onboarding"` is truthy, so `if (await identifyOnboarding(...))` behaves identically at runtime;
|
|
73
|
+
only an explicit `: boolean` annotation needs updating.
|
|
74
|
+
- **⚠️ BEHAVIOR CHANGE: `wire.track()` returns `false` for an event the server DISCARDED.**
|
|
75
|
+
`POST /v1/events` answers HTTP 200 with `{ ok, written, skipped }` and counts a refused event in
|
|
76
|
+
`skipped`, so `reportClientEventsAwait` — which read `res.ok` alone — resolved `true` for an event that
|
|
77
|
+
was thrown away, and `wire.track` then bumped decision revalidation, making every subscribed gate
|
|
78
|
+
re-fetch against a stream the action never entered. It now reads the ack. Backward compatible by
|
|
79
|
+
construction: ONLY an explicit positive `skipped` demotes a 200, so an old server that sends no such
|
|
80
|
+
field, an unparseable body, or a response with no `.json` at all still resolves `true` — the change can
|
|
81
|
+
produce no false negatives.
|
|
82
|
+
- **The join decision is FROZEN at the moment the loader gate opens.** `userContext` feeds the `llm` memo
|
|
83
|
+
and wireai-rn recreates its A2A adapter whenever that identity changes, resetting `contextId`. So a host
|
|
84
|
+
resolving its device key ASYNCHRONOUSLY — the default shape of any host reading its id out of async
|
|
85
|
+
storage — rendered once without it, got `wdev_*` injected, then had the real key land a tick later and
|
|
86
|
+
rebuild the adapter, DROPPING the server-learned session id and RESTARTING the user's onboarding
|
|
87
|
+
mid-flow with an orphaned session left behind. The session-start metadata now reads a frozen snapshot,
|
|
88
|
+
exactly as `startupUserIdRef` already did for the user id. Client events deliberately keep reading the
|
|
89
|
+
LIVE context: they carry no adapter, so late host context enriches them at no risk.
|
|
90
|
+
- **A host device key on ANOTHER surface is no longer silent.** 0.12.2 suppressed the missing-join-key
|
|
91
|
+
warning whenever injection succeeded — right when the host genuinely owns no device id, wrong when it
|
|
92
|
+
owns one and forgot it on this mount. That host went from a loud warning to a silent THIRD id space. Any
|
|
93
|
+
surface constructed with a host-supplied device key (`createAnalytics`, `createWireActivation`,
|
|
94
|
+
`useLifecycleEvents`, `<WireOnboarding userContext={...}>`) now records it, scoped per `appId`, and a mount
|
|
95
|
+
that injected its own key while the process demonstrably owns a host one warns and prints both. It warns
|
|
96
|
+
rather than declining, because declining would leave the onboarding side with no key at all; it does not
|
|
97
|
+
silently ADOPT the other key, because that is the kit guessing which of two ids a host meant on the
|
|
98
|
+
strength of construction order.
|
|
99
|
+
- **Two event queues for one `appId` no longer share one storage slot.** The default key was derived from
|
|
100
|
+
`appId` alone, so two `createAnalytics` instances for one tenant — the documented double-wiring — shared
|
|
101
|
+
ONE persisted backlog while keeping SEPARATE in-memory buffers: each `persist()` overwrote the other's
|
|
102
|
+
blob, a queue draining to empty called `removeItem` and DELETED a sibling's still-pending events, and on
|
|
103
|
+
relaunch the survivors were loaded by both and sent twice. The second claimant of a default key now gets
|
|
104
|
+
`…#2` and a dev warning naming the fix. Only the appId-derived DEFAULT is ever rotated — an EXPLICIT
|
|
105
|
+
`storageKey` is a host declaring which slot it owns (`useLifecycleEvents` does exactly that and
|
|
106
|
+
re-creates its queue on every remount, where rotation would be a worse bug). Only claimed when there IS
|
|
107
|
+
storage; a storage-less queue never touches the key.
|
|
108
|
+
- **A failed device-key hydration is retried instead of cached for the process lifetime.** A transient
|
|
109
|
+
cold-boot storage lock used to be permanent: the settled failure stayed parked on the registry and every
|
|
110
|
+
later caller joined it. A degraded outcome now releases its latches so the next caller starts a fresh read.
|
|
111
|
+
- **The DISCARDED warning covers all FOUR send paths and stops naming a cause the kit never checked.** It
|
|
112
|
+
used to end "the usual cause is an event with a missing or empty `session_id`" — but since
|
|
113
|
+
`ensureCurrentSessionId` shipped, no kit path can emit an event without one, making that the LEAST likely
|
|
114
|
+
explanation and sending every reader to the one place the problem is not. It now reports what the server
|
|
115
|
+
actually said (`skipped`, `written`, and any per-event `reasons` once the server sends them) and admits
|
|
116
|
+
when the endpoint gave no reason. The two `reportClientEvent*` paths — which carry `dropped`, `identify`,
|
|
117
|
+
`client_fallback` and every `wire.track` — join the offline queue and session-start in consuming the ack.
|
|
118
|
+
- **`validators` are looked up by `slot_id` first, then `progress.key`.** Host validators live in the SAME
|
|
119
|
+
key namespace `deriveAnswers` files answers under, so moving the answer key to the slot without moving
|
|
120
|
+
this lookup would have silently UNBOUND every host validator: no error, no warning, values simply stop
|
|
121
|
+
being checked. The fallback keeps a validator map written against today's keys working while a server
|
|
122
|
+
rolls slots out.
|
|
123
|
+
|
|
124
|
+
### Removed
|
|
125
|
+
|
|
126
|
+
- **`DoneBlock`** — self-`@deprecated` since 0.1.x, unused internally since the terminal screen became
|
|
127
|
+
`CompletionView`, and referenced by nothing in `src/` or the playground. The component file is deleted.
|
|
128
|
+
- **`RESERVED_USER_CONTEXT_KEYS`** — read by nothing, including its own module: the namespacing it claims
|
|
129
|
+
to govern is enforced by `EXTRA_KEY_PREFIX`. It was a documentation constant shaped like an invariant.
|
|
130
|
+
|
|
131
|
+
### Internal
|
|
132
|
+
|
|
133
|
+
- The FOUR duplicate `warnInDev` copies (`WireOnboarding`, `config/wireConfigFromEnv`,
|
|
134
|
+
`analytics/analyticsFacade`, `analytics/currentSession`) collapse into `utils/warnInDev`, which returns
|
|
135
|
+
whether it ACTUALLY warned — the property `currentSession`'s once-flag depends on, and the only reason
|
|
136
|
+
its copy differed. The stated rationale for that copy (keeping the module import-free for the
|
|
137
|
+
tree-shaken analytics bundle) had already lapsed: it imports `makeSessionId` from a sibling. The new
|
|
138
|
+
module has no imports of its own, and the `treeShake` canary still holds the real guarantee.
|
|
139
|
+
- The two byte-identical trim helpers (`context/userContext.cleanString`, `activation/wireActivation.clean`)
|
|
140
|
+
collapse into one shared `cleanString`, not re-exported from the barrel.
|
|
141
|
+
- **The canary suite can observe a payload for the first time.** Every `onComplete` mock was typed with
|
|
142
|
+
ZERO parameters, so `OnboardingResult` was structurally unobservable and every assertion could only be a
|
|
143
|
+
call count — the mechanism behind "500 green tests, zero data". `flow.test.tsx` now captures the argument
|
|
144
|
+
and asserts the answers map end to end. The scripted `wireai-rn` mock also appends the USER turn to the
|
|
145
|
+
thread, as the real SDK does; without it the mocked thread was assistant-only, so `deriveAnswers` had
|
|
146
|
+
nothing to pair and `answers` was always `{}` under test regardless of the implementation.
|
|
147
|
+
- `mountCanary` gained `rerender()`, the seam for a prop that arrives LATE. The churn loop runs entirely
|
|
148
|
+
inside the initial `act`, so without it there was no way to re-render a mount after an async gate had
|
|
149
|
+
settled — and the whole "value changed mid-flow" defect class was untestable.
|
|
150
|
+
|
|
6
151
|
## [0.12.2] — 2026-07-27
|
|
7
152
|
|
|
8
153
|
The auto-join patch. 0.12.1 taught the kit to complain about a missing join key; this one teaches it
|
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
234
234
|
| `onComplete` | `(result: { answers, raw }) => void` | Fires when the user taps the terminal recap's CTA. **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
|
-
| `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. |
|
|
237
|
+
| `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. When the backend sends a `progress.slot_id` for a screen, the kit looks up that slot FIRST and falls back to the question key, so a validator map written against today's keys keeps working. |
|
|
238
238
|
| `onSkip` | `() => void` | User skipped. |
|
|
239
239
|
| `onError` | `(err) => void` | Backend error/timeout — host owns recovery (e.g. route to a static flow). Without it, the kit shows an inline retry. |
|
|
240
240
|
| `onEvent` | `(e: OnboardingEvent) => void` | `started` / `turn` / `error` — recover per-turn analytics since the kit owns the loop. |
|
|
@@ -247,7 +247,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
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
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
|
+
| `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)). |
|
|
251
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). |
|
|
252
252
|
|
|
253
253
|
## Helpers (the reusable substrate)
|
|
@@ -340,7 +340,9 @@ const contextIdRef = useRef<string>();
|
|
|
340
340
|
await identifyOnboarding({ config, userId: newUser.id, contextId: contextIdRef.current });
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
If you passed `storage` to `<WireOnboarding>`, you can omit `contextId` and hand `identifyOnboarding` the same `storage` (plus `appId` or `persistKey`) and it recovers the persisted session id itself. That works while the session is still persisted (before completion clears it), so the captured-`contextId` path above is the safe one post-completion. `identifyOnboarding` is fire-and-forget and never throws
|
|
343
|
+
If you passed `storage` to `<WireOnboarding>`, you can omit `contextId` and hand `identifyOnboarding` the same `storage` (plus `appId` or `persistKey`) and it recovers the persisted session id itself. That works while the session is still persisted (before completion clears it), so the captured-`contextId` path above is the safe one post-completion. `identifyOnboarding` is fire-and-forget and never throws. Since 0.13.0 it resolves the id SPACE it bound rather than a bare boolean: `"onboarding"` when it attached to the A2A `contextId` (the one that attributes the funnel), `"app_session"` when you opted into `allowAppSessionFallback` and it fell back to the live per-open app session, and `false` when it could not bind at all (no user id, no server url, or no resolvable session). `"onboarding"` is truthy, so an existing `if (await identifyOnboarding(...))` behaves identically.
|
|
344
|
+
|
|
345
|
+
⚠️ **The app-session fallback is now opt-in and off by default.** Before 0.13.0 it ran unconditionally: with no captured `contextId` it posted the per-OPEN session id in the `session_id` field — which on this endpoint means the ONBOARDING session — and returned `true`. Those are two different id spaces sharing one wire field, so the row it wrote could never join the onboarding funnel while the caller was told it had worked.
|
|
344
346
|
|
|
345
347
|
**No PII.** The onboarding `userId` prop is length-bound only; the kit cannot detect an email for you. Keep it opaque, the same rule as the onboarding `userContext` prop.
|
|
346
348
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-CF_eHwzC.mjs';
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
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
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-_GynvhzT.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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-_GynvhzT.mjs';
|
|
4
4
|
import '../types-CNUqMK0D.mjs';
|
|
5
5
|
import '../types-BKfpdZzX.mjs';
|
|
6
6
|
import '../types-BcmagF6K.mjs';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DsRe4epC.js';
|
|
2
|
-
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
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
|
|
2
|
+
import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-D7zabMXK.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 resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-D7zabMXK.js';
|
|
4
4
|
import '../types-Buj9Lw9t.js';
|
|
5
5
|
import '../types-BKfpdZzX.js';
|
|
6
6
|
import '../types-BcmagF6K.js';
|
package/dist/analytics/index.js
CHANGED
|
@@ -413,28 +413,35 @@ var buildEventsRequest = (target, events) => {
|
|
|
413
413
|
return null;
|
|
414
414
|
}
|
|
415
415
|
};
|
|
416
|
-
var
|
|
417
|
-
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
418
|
-
if (typeof console === "undefined" || !console.warn) return;
|
|
416
|
+
var readEventsAck = async (res) => {
|
|
419
417
|
try {
|
|
420
418
|
const json = res == null ? void 0 : res.json;
|
|
421
|
-
if (typeof json !== "function") return;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
);
|
|
428
|
-
}).catch(() => {
|
|
429
|
-
});
|
|
419
|
+
if (typeof json !== "function") return void 0;
|
|
420
|
+
const body = await Promise.resolve(json.call(res));
|
|
421
|
+
const skipped = body == null ? void 0 : body.skipped;
|
|
422
|
+
if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
|
|
423
|
+
const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => e == null ? void 0 : e.reason).filter((r) => typeof r === "string") : [];
|
|
424
|
+
return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
|
|
430
425
|
} catch {
|
|
426
|
+
return void 0;
|
|
431
427
|
}
|
|
432
428
|
};
|
|
429
|
+
var describeDiscarded = (ack) => `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${ack.skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The server reported: skipped=${ack.skipped}${ack.written !== void 0 ? `, written=${ack.written}` : ""}` + (ack.reasons.length > 0 ? `, reasons: ${ack.reasons.join(", ")}.` : ". It gave no reason (the endpoint folds every rejection into one count), so check the server's ingest log for this request rather than guessing.");
|
|
430
|
+
var warnOnSkippedEvents = (res) => {
|
|
431
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
432
|
+
if (typeof console === "undefined" || !console.warn) return;
|
|
433
|
+
void readEventsAck(res).then((ack) => {
|
|
434
|
+
if (!ack || ack.skipped <= 0) return;
|
|
435
|
+
console.warn(describeDiscarded(ack));
|
|
436
|
+
});
|
|
437
|
+
};
|
|
433
438
|
var reportClientEvents = (target, events) => {
|
|
434
439
|
try {
|
|
435
440
|
const req = buildEventsRequest(target, events);
|
|
436
441
|
if (!req) return;
|
|
437
|
-
void fetch(req.url, req.init).
|
|
442
|
+
void fetch(req.url, req.init).then((res) => {
|
|
443
|
+
warnOnSkippedEvents(res);
|
|
444
|
+
}).catch(() => {
|
|
438
445
|
});
|
|
439
446
|
} catch {
|
|
440
447
|
}
|
|
@@ -445,13 +452,28 @@ var reportClientEventsAwait = async (target, events) => {
|
|
|
445
452
|
const req = buildEventsRequest(target, events);
|
|
446
453
|
if (!req) return false;
|
|
447
454
|
const res = await fetch(req.url, req.init);
|
|
448
|
-
|
|
455
|
+
if (!res || !res.ok) return false;
|
|
456
|
+
const ack = await readEventsAck(res);
|
|
457
|
+
if (!ack || ack.skipped <= 0) return true;
|
|
458
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
|
|
459
|
+
console.warn(describeDiscarded(ack));
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
449
462
|
} catch {
|
|
450
463
|
return false;
|
|
451
464
|
}
|
|
452
465
|
};
|
|
453
466
|
var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
|
|
454
467
|
|
|
468
|
+
// src/utils/warnInDev.ts
|
|
469
|
+
var warnInDev = (message) => {
|
|
470
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
|
|
471
|
+
console.warn(message);
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
};
|
|
476
|
+
|
|
455
477
|
// src/analytics/currentSession.ts
|
|
456
478
|
var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
|
|
457
479
|
"@wireai/activation:currentSessionId"
|
|
@@ -466,13 +488,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
|
|
|
466
488
|
var resetCurrentSessionId = () => {
|
|
467
489
|
globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
|
|
468
490
|
};
|
|
469
|
-
var warnInDev = (message) => {
|
|
470
|
-
if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
|
|
471
|
-
console.warn(message);
|
|
472
|
-
return true;
|
|
473
|
-
}
|
|
474
|
-
return false;
|
|
475
|
-
};
|
|
476
491
|
var MINT_WARNING = "[wireai] No app-open session was registered, so a session id was minted for this event (the server drops an event that has no session_id, and still answers 200). Mount useLifecycleEvents at your app root so events correlate to a real app-open.";
|
|
477
492
|
var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
478
493
|
"@wireai/activation:currentSessionIdMintWarned"
|
|
@@ -820,6 +835,31 @@ var DEFAULTS = {
|
|
|
820
835
|
maxRetries: 6
|
|
821
836
|
};
|
|
822
837
|
var READ_TIMEOUT_MS = 1500;
|
|
838
|
+
var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
|
|
839
|
+
var queueKeyGlobal = globalThis;
|
|
840
|
+
var claimedQueueKeys = () => {
|
|
841
|
+
const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
|
|
842
|
+
if (existing) return existing;
|
|
843
|
+
const created = /* @__PURE__ */ new Set();
|
|
844
|
+
queueKeyGlobal[QUEUE_KEY_SLOT] = created;
|
|
845
|
+
return created;
|
|
846
|
+
};
|
|
847
|
+
var claimQueueKey = (preferred, explicit) => {
|
|
848
|
+
const claimed = claimedQueueKeys();
|
|
849
|
+
if (explicit || !claimed.has(preferred)) {
|
|
850
|
+
claimed.add(preferred);
|
|
851
|
+
return preferred;
|
|
852
|
+
}
|
|
853
|
+
let ordinal = 2;
|
|
854
|
+
while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
|
|
855
|
+
const key = `${preferred}#${ordinal}`;
|
|
856
|
+
claimed.add(key);
|
|
857
|
+
warnInDev(
|
|
858
|
+
`[wireai] a second event queue was created for the same appId, and "${preferred}" is already claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, delete each other's pending events when one drains to empty, and double-send on relaunch, so this queue was given "${key}" instead. Prefer ONE analytics instance per app; if you really need two, pass an explicit \`storageKey\` to each so the slots are yours to reason about.`
|
|
859
|
+
);
|
|
860
|
+
return key;
|
|
861
|
+
};
|
|
862
|
+
var resetEventQueueKeys = () => claimedQueueKeys().clear();
|
|
823
863
|
var withTimeout = (p, ms) => {
|
|
824
864
|
let timer;
|
|
825
865
|
const timeout = new Promise((resolve) => {
|
|
@@ -851,7 +891,8 @@ var createEventQueue = (options) => {
|
|
|
851
891
|
var _a2, _b, _c, _d, _e, _f, _g;
|
|
852
892
|
const target = options.target;
|
|
853
893
|
const storage = options.storage;
|
|
854
|
-
const
|
|
894
|
+
const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
|
|
895
|
+
const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
|
|
855
896
|
const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
|
|
856
897
|
const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
|
|
857
898
|
const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
|
|
@@ -1102,6 +1143,29 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
|
|
|
1102
1143
|
return result;
|
|
1103
1144
|
};
|
|
1104
1145
|
|
|
1146
|
+
// src/identity/identityRecord.ts
|
|
1147
|
+
var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
|
|
1148
|
+
var provenanceGlobal = globalThis;
|
|
1149
|
+
var provenanceRegistry = () => {
|
|
1150
|
+
const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
|
|
1151
|
+
if (existing) return existing;
|
|
1152
|
+
const created = { host: /* @__PURE__ */ new Map() };
|
|
1153
|
+
provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
|
|
1154
|
+
return created;
|
|
1155
|
+
};
|
|
1156
|
+
var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
|
|
1157
|
+
var resolveIdentity = (input) => {
|
|
1158
|
+
var _a2;
|
|
1159
|
+
if (typeof input.value !== "string") return void 0;
|
|
1160
|
+
const value = input.value.trim();
|
|
1161
|
+
if (!value) return void 0;
|
|
1162
|
+
const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
|
|
1163
|
+
{
|
|
1164
|
+
provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
|
|
1165
|
+
}
|
|
1166
|
+
return { value, space: input.space, source: input.source, durable };
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1105
1169
|
// src/context/deviceId.ts
|
|
1106
1170
|
var AUTO_DEVICE_ID_PREFIX = "wdev_";
|
|
1107
1171
|
var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
|
|
@@ -1124,24 +1188,37 @@ var startHydration = (registry, appId, storage, minted) => {
|
|
|
1124
1188
|
const existing = registry.pending.get(appId);
|
|
1125
1189
|
if (existing) return existing;
|
|
1126
1190
|
const slot = deviceIdStorageKey(appId);
|
|
1127
|
-
const
|
|
1191
|
+
const degraded = () => {
|
|
1128
1192
|
var _a2;
|
|
1129
|
-
return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
|
|
1193
|
+
return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
|
|
1130
1194
|
};
|
|
1195
|
+
const adopted = (value) => ({ value, durable: true });
|
|
1131
1196
|
let run;
|
|
1132
1197
|
try {
|
|
1133
1198
|
run = Promise.resolve(storage.getItem(slot)).then((saved) => {
|
|
1134
1199
|
const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
|
|
1135
1200
|
if (persisted) {
|
|
1136
1201
|
registry.keys.set(appId, persisted);
|
|
1137
|
-
return persisted;
|
|
1202
|
+
return adopted(persisted);
|
|
1138
1203
|
}
|
|
1139
|
-
return Promise.resolve(storage.setItem(slot, minted)).then(
|
|
1140
|
-
|
|
1204
|
+
return Promise.resolve(storage.setItem(slot, minted)).then(
|
|
1205
|
+
() => {
|
|
1206
|
+
var _a2;
|
|
1207
|
+
return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
|
|
1208
|
+
},
|
|
1209
|
+
degraded
|
|
1210
|
+
);
|
|
1211
|
+
}).catch(degraded);
|
|
1141
1212
|
} catch {
|
|
1142
|
-
run = Promise.resolve(
|
|
1213
|
+
run = Promise.resolve(degraded());
|
|
1143
1214
|
}
|
|
1144
1215
|
registry.pending.set(appId, run);
|
|
1216
|
+
void run.then((outcome) => {
|
|
1217
|
+
var _a2;
|
|
1218
|
+
if (outcome.durable) return;
|
|
1219
|
+
(_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
|
|
1220
|
+
registry.hydrating.delete(appId);
|
|
1221
|
+
});
|
|
1145
1222
|
return run;
|
|
1146
1223
|
};
|
|
1147
1224
|
var resolveAutoDeviceKey = (opts = {}) => {
|
|
@@ -1169,11 +1246,6 @@ var resetAutoDeviceKeys = () => {
|
|
|
1169
1246
|
};
|
|
1170
1247
|
|
|
1171
1248
|
// src/analytics/analyticsFacade.ts
|
|
1172
|
-
var warnInDev2 = (message) => {
|
|
1173
|
-
if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
|
|
1174
|
-
console.warn(message);
|
|
1175
|
-
}
|
|
1176
|
-
};
|
|
1177
1249
|
var createAnalytics = (config, options = {}) => {
|
|
1178
1250
|
var _a2, _b, _c;
|
|
1179
1251
|
const resolveSessionId = () => {
|
|
@@ -1181,13 +1253,19 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1181
1253
|
return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
|
|
1182
1254
|
};
|
|
1183
1255
|
if (config.sessionId) {
|
|
1184
|
-
|
|
1256
|
+
warnInDev(
|
|
1185
1257
|
"[wireai] createAnalytics({ sessionId }) PINS every event from this instance to that one frozen id and opts out of the live per-open session (app.session_started) \u2014 lifecycle analytics collapse onto a single device-scoped id. Remove it unless your host runs its own session lifecycle."
|
|
1186
1258
|
);
|
|
1187
1259
|
}
|
|
1188
1260
|
const detectedAppVersion = detectAppVersion();
|
|
1189
1261
|
let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
|
|
1190
1262
|
const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
|
|
1263
|
+
resolveIdentity({
|
|
1264
|
+
value: hostDeviceKeyAtInit,
|
|
1265
|
+
space: "device",
|
|
1266
|
+
source: "host",
|
|
1267
|
+
scope: config.appId
|
|
1268
|
+
});
|
|
1191
1269
|
const autoDeviceKeyOptions = {
|
|
1192
1270
|
appId: config.appId,
|
|
1193
1271
|
// A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
|
|
@@ -1233,7 +1311,7 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1233
1311
|
};
|
|
1234
1312
|
const guardUserId = (clean) => {
|
|
1235
1313
|
if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
|
|
1236
|
-
|
|
1314
|
+
warnInDev(
|
|
1237
1315
|
"[wireai] identify() was called with an email-shaped id. A raw email must NOT be the opaque user_id (PII leak) \u2014 pass it as userContext.userEmail instead. Binding was skipped. Set allowEmailAsUserId:true on createAnalytics if your user id genuinely is an email."
|
|
1238
1316
|
);
|
|
1239
1317
|
return void 0;
|
|
@@ -1354,6 +1432,7 @@ exports.reportClientEvents = reportClientEvents;
|
|
|
1354
1432
|
exports.reportClientEventsAwait = reportClientEventsAwait;
|
|
1355
1433
|
exports.resetAutoDeviceKeys = resetAutoDeviceKeys;
|
|
1356
1434
|
exports.resetCurrentSessionId = resetCurrentSessionId;
|
|
1435
|
+
exports.resetEventQueueKeys = resetEventQueueKeys;
|
|
1357
1436
|
exports.resolveAutoDeviceKey = resolveAutoDeviceKey;
|
|
1358
1437
|
exports.screenTrackingHandler = screenTrackingHandler;
|
|
1359
1438
|
exports.setCurrentSessionId = setCurrentSessionId;
|