@wireai/activation 0.12.0 → 0.12.2
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 +13 -10
- package/CHANGELOG.md +130 -0
- package/INTEGRATION_PROMPT.md +14 -3
- package/README.md +40 -2
- package/dist/analytics/index.d.mts +5 -4
- package/dist/analytics/index.d.ts +5 -4
- package/dist/analytics/index.js +39 -24
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +39 -24
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +16 -0
- package/dist/coachmarks/index.d.ts +16 -0
- package/dist/coachmarks/index.js +19 -13
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +19 -13
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-DsSDHqor.d.mts → currentSession-BlCeDP0f.d.mts} +59 -5
- package/dist/{currentSession-D6RiVtc8.d.ts → currentSession-BxEB37xt.d.ts} +59 -5
- package/dist/index.d.mts +10 -20
- package/dist/index.d.ts +10 -20
- package/dist/index.js +301 -187
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +300 -188
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.js +41 -19
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +41 -19
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.js +41 -19
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +41 -19
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js +15 -6
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +15 -6
- package/dist/showcase/index.mjs.map +1 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/src/WireOnboarding.tsx +140 -5
- package/src/activation/wireActivation.ts +8 -1
- package/src/analytics/analyticsFacade.ts +15 -11
- package/src/analytics/reportClientEvent.ts +11 -7
- package/src/coachmarks/runtime.ts +53 -17
- package/src/config/wireConfigFromEnv.ts +46 -2
- package/src/context/deviceId.ts +82 -18
- package/src/index.ts +9 -1
- package/src/questionnaire/runtime.ts +1 -0
- package/src/questionnaire/useQuestionnaireGate.ts +8 -3
- package/src/reviews/runtime.ts +68 -7
- package/src/reviews/useReviewGate.ts +8 -3
- package/src/session-analytics/useLifecycleEvents.ts +57 -27
- package/src/session-analytics/useSessionStart.ts +37 -1
- 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
|
|
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).
|
|
@@ -51,7 +49,9 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
|
|
|
51
49
|
- `mergeTheme`, `defaultOnboardingTheme`, `OnboardingThemeProvider`, `useOnboardingTheme`.
|
|
52
50
|
- `defaultIllustrations`: dependency-free fallback art; spread your own over it.
|
|
53
51
|
- `createRevenueCatBridge({ analytics, entitlementId })`: the RevenueCat purchase funnel (see "RevenueCat" below).
|
|
54
|
-
- `activationJoinContext(deviceKey)`: builds the `userContext` value that joins an onboarding session to the app's later events.
|
|
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.
|
|
55
55
|
- Types: `OnboardingTheme`, `OnboardingResult`, `OnboardingEvent`, `WireOnboardingConfig`, `WireOnboardingProps`, `StepValidator`, `OnboardingCopy`, `IllustrationRegistry`.
|
|
56
56
|
|
|
57
57
|
## `<WireOnboarding>` props
|
|
@@ -90,9 +90,10 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
|
|
|
90
90
|
| `metadata` | `Record<string, unknown>` | no | Merged into every A2A request (e.g. install attribution). The kit reserves `sessionId` + `supportedComponents`; do not override them. |
|
|
91
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). |
|
|
92
92
|
|
|
93
|
-
## App events
|
|
93
|
+
## App events (root export)
|
|
94
94
|
|
|
95
|
-
|
|
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`.
|
|
96
97
|
|
|
97
98
|
- `WIRE_ONBOARDING_EVENTS` + `toAnalyticsEvent(event)`: canonical `wire_onboarding_*` funnel names; log via the app's own analytics.
|
|
98
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.
|
|
@@ -107,8 +108,9 @@ The app-event / analytics API is exported from the ROOT (`@wireai/activation`),
|
|
|
107
108
|
4. **Config**: set `EXPO_PUBLIC_WIREAI_API_KEY` / `_SERVER_URL` / `_APP_ID`; build via `wireConfigFromEnv({ appId })`.
|
|
108
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.
|
|
109
110
|
6. **Theme**: `themeFromBrand({ primary })` or a full `Partial<OnboardingTheme>`; for dark/light, pick the theme by the app's theme state.
|
|
110
|
-
7. **Render + continuation**: drop `<WireOnboarding>` into the signup flow; `onComplete` → persist + navigate, `onSkip` → navigate.
|
|
111
|
-
8. **
|
|
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.
|
|
112
114
|
|
|
113
115
|
## Review firing (do not hand-roll the decision fetch)
|
|
114
116
|
|
|
@@ -141,7 +143,8 @@ the kit never forces a rebuild); the adapter types the RevenueCat objects struct
|
|
|
141
143
|
the real ones you already have.
|
|
142
144
|
|
|
143
145
|
```tsx
|
|
144
|
-
import {
|
|
146
|
+
import { createRevenueCatBridge, activationJoinContext } from "@wireai/activation";
|
|
147
|
+
import { createAnalytics } from "@wireai/activation/analytics";
|
|
145
148
|
|
|
146
149
|
const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userContext: { deviceKey } });
|
|
147
150
|
const revenuecat = createRevenueCatBridge({ analytics, entitlementId: "pro" });
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,136 @@
|
|
|
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.12.2] — 2026-07-27
|
|
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
|
+
|
|
6
136
|
## [0.12.0] — 2026-07-27
|
|
7
137
|
|
|
8
138
|
The release Myelino 2.2.0 pins. Ships the RevenueCat path (#52), the identity/counting audit,
|
package/INTEGRATION_PROMPT.md
CHANGED
|
@@ -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.
|
|
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,7 @@ 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
|
-
|
|
67
|
+
10. RevenueCat (only if my app sells subscriptions through `react-native-purchases`): wire my paywall
|
|
57
68
|
to the same event stream with `createRevenueCatBridge({ analytics, entitlementId: "<my
|
|
58
69
|
entitlement, e.g. pro>" })` from `@wireai/activation`. Do NOT add or import
|
|
59
70
|
`react-native-purchases` in the kit path; the adapter types the RevenueCat objects structurally,
|
|
@@ -65,7 +76,7 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
65
76
|
onboarding on `user_context.device_key` and never on `session_id` (those are separate id spaces
|
|
66
77
|
and intersecting them returns zero rows). If my app has no single stable device key yet, say so
|
|
67
78
|
instead of inventing one.
|
|
68
|
-
|
|
79
|
+
11. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths.
|
|
69
80
|
|
|
70
81
|
Report back: the files you changed (path:line), the typecheck result, and anything you couldn't
|
|
71
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>` |
|
|
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)
|
|
@@ -388,6 +389,34 @@ Omit it. Pass one only if your host runs its own session lifecycle and owns the
|
|
|
388
389
|
correlate on. This is a different field from the per-open `sessionId` that `useWireActivation` and
|
|
389
390
|
`reportAppEvent` READ (that one is the live id, and reading it is always fine).
|
|
390
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
|
+
|
|
391
420
|
## Session mapping (know when a user opens the app again)
|
|
392
421
|
|
|
393
422
|
Onboarding runs once, on the first launch. Session mapping is the other half: every time the
|
|
@@ -990,7 +1019,8 @@ If you sell subscriptions with [RevenueCat](https://www.revenuecat.com/), the pa
|
|
|
990
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.
|
|
991
1020
|
|
|
992
1021
|
```tsx
|
|
993
|
-
import {
|
|
1022
|
+
import { createRevenueCatBridge } from "@wireai/activation";
|
|
1023
|
+
import { createAnalytics } from "@wireai/activation/analytics";
|
|
994
1024
|
|
|
995
1025
|
const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userContext: { deviceKey } });
|
|
996
1026
|
const revenuecat = createRevenueCatBridge({ analytics, entitlementId: "pro" });
|
|
@@ -1038,6 +1068,14 @@ const analytics = createAnalytics({ serverUrl, apiKey, storage, appId, userConte
|
|
|
1038
1068
|
|
|
1039
1069
|
Skip the onboarding half and the purchase events are still valid on their own, they just cannot be attributed back to an onboarding.
|
|
1040
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
|
+
|
|
1041
1079
|
## Feature controls (per-module kill switches)
|
|
1042
1080
|
|
|
1043
1081
|
Every activation surface has a switch you can flip from the dashboard "in case of something":
|
|
@@ -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 resolveAutoDeviceKey, F as setCurrentSessionId, G as toAnalyticsEvent } from '../currentSession-
|
|
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';
|
|
@@ -114,8 +114,9 @@ type CreateAnalyticsConfig = {
|
|
|
114
114
|
* analytics then collapse onto a single device-scoped session — one "first open", forever.
|
|
115
115
|
*
|
|
116
116
|
* OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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.
|
|
119
120
|
*/
|
|
120
121
|
sessionId?: string;
|
|
121
122
|
/** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */
|
|
@@ -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 resolveAutoDeviceKey, F as setCurrentSessionId, G as toAnalyticsEvent } from '../currentSession-
|
|
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';
|
|
@@ -114,8 +114,9 @@ type CreateAnalyticsConfig = {
|
|
|
114
114
|
* analytics then collapse onto a single device-scoped session — one "first open", forever.
|
|
115
115
|
*
|
|
116
116
|
* OMIT IT — that is the correct default. Without it the kit reuses the live per-open session, and
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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.
|
|
119
120
|
*/
|
|
120
121
|
sessionId?: string;
|
|
121
122
|
/** Tenant/app id used to namespace the queue's default storage key (`wireai:evtq:<appId>`). */
|
package/dist/analytics/index.js
CHANGED
|
@@ -414,17 +414,17 @@ var buildEventsRequest = (target, events) => {
|
|
|
414
414
|
}
|
|
415
415
|
};
|
|
416
416
|
var warnOnSkippedEvents = (res) => {
|
|
417
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
418
|
+
if (typeof console === "undefined" || !console.warn) return;
|
|
417
419
|
try {
|
|
418
420
|
const json = res == null ? void 0 : res.json;
|
|
419
421
|
if (typeof json !== "function") return;
|
|
420
422
|
void Promise.resolve(json.call(res)).then((body) => {
|
|
421
423
|
const skipped = body == null ? void 0 : body.skipped;
|
|
422
424
|
if (typeof skipped !== "number" || skipped <= 0) return;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
);
|
|
427
|
-
}
|
|
425
|
+
console.warn(
|
|
426
|
+
`[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The usual cause is an event with a missing or empty session_id.`
|
|
427
|
+
);
|
|
428
428
|
}).catch(() => {
|
|
429
429
|
});
|
|
430
430
|
} catch {
|
|
@@ -1119,6 +1119,31 @@ var autoDeviceKeyRegistry = () => {
|
|
|
1119
1119
|
deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
|
|
1120
1120
|
return created;
|
|
1121
1121
|
};
|
|
1122
|
+
var startHydration = (registry, appId, storage, minted) => {
|
|
1123
|
+
if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
|
|
1124
|
+
const existing = registry.pending.get(appId);
|
|
1125
|
+
if (existing) return existing;
|
|
1126
|
+
const slot = deviceIdStorageKey(appId);
|
|
1127
|
+
const settled = () => {
|
|
1128
|
+
var _a2;
|
|
1129
|
+
return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
|
|
1130
|
+
};
|
|
1131
|
+
let run;
|
|
1132
|
+
try {
|
|
1133
|
+
run = Promise.resolve(storage.getItem(slot)).then((saved) => {
|
|
1134
|
+
const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
|
|
1135
|
+
if (persisted) {
|
|
1136
|
+
registry.keys.set(appId, persisted);
|
|
1137
|
+
return persisted;
|
|
1138
|
+
}
|
|
1139
|
+
return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
|
|
1140
|
+
}).catch(settled);
|
|
1141
|
+
} catch {
|
|
1142
|
+
run = Promise.resolve(settled());
|
|
1143
|
+
}
|
|
1144
|
+
registry.pending.set(appId, run);
|
|
1145
|
+
return run;
|
|
1146
|
+
};
|
|
1122
1147
|
var resolveAutoDeviceKey = (opts = {}) => {
|
|
1123
1148
|
var _a2, _b;
|
|
1124
1149
|
const registry = autoDeviceKeyRegistry();
|
|
@@ -1131,25 +1156,16 @@ var resolveAutoDeviceKey = (opts = {}) => {
|
|
|
1131
1156
|
const storage = opts.storage;
|
|
1132
1157
|
if (storage && !registry.hydrating.has(appId)) {
|
|
1133
1158
|
registry.hydrating.add(appId);
|
|
1134
|
-
|
|
1135
|
-
const minted = id;
|
|
1136
|
-
try {
|
|
1137
|
-
void Promise.resolve(storage.getItem(slot)).then((saved) => {
|
|
1138
|
-
const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
|
|
1139
|
-
if (persisted) registry.keys.set(appId, persisted);
|
|
1140
|
-
else void Promise.resolve(storage.setItem(slot, minted)).catch(() => {
|
|
1141
|
-
});
|
|
1142
|
-
}).catch(() => {
|
|
1143
|
-
});
|
|
1144
|
-
} catch {
|
|
1145
|
-
}
|
|
1159
|
+
void startHydration(registry, appId, storage, id);
|
|
1146
1160
|
}
|
|
1147
1161
|
return (_b = registry.keys.get(appId)) != null ? _b : id;
|
|
1148
1162
|
};
|
|
1149
1163
|
var resetAutoDeviceKeys = () => {
|
|
1164
|
+
var _a2;
|
|
1150
1165
|
const registry = autoDeviceKeyRegistry();
|
|
1151
1166
|
registry.keys.clear();
|
|
1152
1167
|
registry.hydrating.clear();
|
|
1168
|
+
(_a2 = registry.pending) == null ? void 0 : _a2.clear();
|
|
1153
1169
|
};
|
|
1154
1170
|
|
|
1155
1171
|
// src/analytics/analyticsFacade.ts
|
|
@@ -1159,11 +1175,10 @@ var warnInDev2 = (message) => {
|
|
|
1159
1175
|
}
|
|
1160
1176
|
};
|
|
1161
1177
|
var createAnalytics = (config, options = {}) => {
|
|
1162
|
-
var _a2, _b, _c
|
|
1163
|
-
const instanceSessionId = (_a2 = config.sessionId) != null ? _a2 : makeSessionId();
|
|
1178
|
+
var _a2, _b, _c;
|
|
1164
1179
|
const resolveSessionId = () => {
|
|
1165
|
-
var _a3
|
|
1166
|
-
return (
|
|
1180
|
+
var _a3;
|
|
1181
|
+
return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
|
|
1167
1182
|
};
|
|
1168
1183
|
if (config.sessionId) {
|
|
1169
1184
|
warnInDev2(
|
|
@@ -1171,8 +1186,8 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1171
1186
|
);
|
|
1172
1187
|
}
|
|
1173
1188
|
const detectedAppVersion = detectAppVersion();
|
|
1174
|
-
let userContext = { ...(
|
|
1175
|
-
const hostDeviceKeyAtInit = typeof ((
|
|
1189
|
+
let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
|
|
1190
|
+
const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
|
|
1176
1191
|
const autoDeviceKeyOptions = {
|
|
1177
1192
|
appId: config.appId,
|
|
1178
1193
|
// A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
|
|
@@ -1195,7 +1210,7 @@ var createAnalytics = (config, options = {}) => {
|
|
|
1195
1210
|
envelope,
|
|
1196
1211
|
...options
|
|
1197
1212
|
});
|
|
1198
|
-
let boundUserId = sanitizeUserId((
|
|
1213
|
+
let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
|
|
1199
1214
|
const storageKey = analyticsUserIdStorageKey(config.appId);
|
|
1200
1215
|
if (config.storage) {
|
|
1201
1216
|
void config.storage.getItem(storageKey).then((saved) => {
|