@wireai/activation 0.13.6 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -9
- package/CHANGELOG.md +227 -1
- package/INTEGRATION_PROMPT.md +7 -4
- package/README.md +15 -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 +31 -7
- package/dist/index.d.ts +31 -7
- package/dist/index.js +643 -539
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +643 -539
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.js +14 -6
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +14 -6
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.js +14 -6
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +14 -6
- 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 +48 -8
- package/src/activation/useWireActivation.ts +14 -1
- package/src/activation/wireActivation.ts +68 -2
- package/src/analytics/analyticsFacade.ts +24 -0
- package/src/analytics/currentSession.ts +3 -2
- 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 +41 -5
- package/src/reviews/runtime.ts +75 -23
- 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
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* useSessionStart — the optional convenience hook that fires {@link reportSessionStart} for you.
|
|
3
3
|
*
|
|
4
4
|
* Two firing moments, mirroring the reference apps' session-counter semantics:
|
|
5
|
-
* • ON MOUNT — the app opened (cold start or the provider first rendered).
|
|
5
|
+
* • ON MOUNT — the app opened (cold start or the provider first rendered). When the kit's AUTO
|
|
6
|
+
* device key is the one in play (no host `deviceKey`, `config.storage` present) the fire waits
|
|
7
|
+
* on one storage read, so the event carries the PERSISTED key rather than a fresh per-launch
|
|
8
|
+
* mint, and it refuses the key outright if that read settles non-durable (0.14.0).
|
|
6
9
|
* • ON FOREGROUND after a real background — when AppState returns to `active` having been
|
|
7
10
|
* backgrounded for at least {@link BACKGROUND_SESSION_MS} (30 min), that's a NEW open, so a
|
|
8
11
|
* fresh session fires. A quick app-switch (under the threshold) does NOT count as a new open.
|
|
@@ -26,7 +29,7 @@ import { useEffect, useRef } from "react";
|
|
|
26
29
|
import { AppState, Platform, type AppStateStatus } from "react-native";
|
|
27
30
|
|
|
28
31
|
import type { ClientEventTarget } from "../analytics/reportClientEvent";
|
|
29
|
-
import {
|
|
32
|
+
import { hydrateDeviceIdentity } from "../context/deviceId";
|
|
30
33
|
import { collectDeviceContext } from "../device/deviceContext";
|
|
31
34
|
import type { WireOnboardingStorage } from "../session/persistedSession";
|
|
32
35
|
import { reportSessionStart } from "./reportSessionStart";
|
|
@@ -44,8 +47,9 @@ export interface SessionStartConfig {
|
|
|
44
47
|
appVersion?: string;
|
|
45
48
|
/** Tenant/app id — namespaces the auto `device_key` fallback below. */
|
|
46
49
|
appId?: string;
|
|
47
|
-
/** Host storage (AsyncStorage subset). Present → `app.session_started`
|
|
48
|
-
* persisted auto `device_key` when the host passes none. Absent
|
|
50
|
+
/** Host storage (AsyncStorage subset). Present AND actually persisting → `app.session_started`
|
|
51
|
+
* falls back to the kit's persisted auto `device_key` when the host passes none. Absent, or an
|
|
52
|
+
* adapter that throws / rejects → no fallback (a per-launch key is worse than none, see below). */
|
|
49
53
|
storage?: WireOnboardingStorage;
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -78,8 +82,13 @@ export const useSessionStart = (
|
|
|
78
82
|
latest.current = { config, options };
|
|
79
83
|
|
|
80
84
|
useEffect(() => {
|
|
85
|
+
// Set by the cleanup below: an in-flight storage read must not fire an app-open for a mount that
|
|
86
|
+
// is already gone. Declared first because the async fire path closes over it.
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
|
|
81
89
|
/**
|
|
82
|
-
* The `device_key` this open rides under. A host-supplied id always wins
|
|
90
|
+
* The `device_key` this open rides under. A host-supplied id always wins; the auto id arrives
|
|
91
|
+
* already vetted from `openAutoDeviceKey` below (a non-durable one never gets here).
|
|
83
92
|
*
|
|
84
93
|
* BACKPORTED FROM `useLifecycleEvents` (which had this and this hook did not): `min_sessions` is
|
|
85
94
|
* computed server-side by counting distinct `app.session_started` grouped by
|
|
@@ -88,21 +97,50 @@ export const useSessionStart = (
|
|
|
88
97
|
* spaces, and a counter that could never increase. The two session-start paths must not diverge
|
|
89
98
|
* on the thing the firing rule reads.
|
|
90
99
|
*
|
|
91
|
-
* ONLY WITH `storage
|
|
92
|
-
*
|
|
93
|
-
* `min_sessions` in the other
|
|
100
|
+
* ONLY WITH `storage` THAT ACTUALLY WORKED, for the same reason as the sibling hook: without
|
|
101
|
+
* persistence — or with an adapter that threw / rejected — the auto id is per-LAUNCH, and a
|
|
102
|
+
* per-launch key makes every open look like a new device, corrupting `min_sessions` in the other
|
|
103
|
+
* direction. Both cases arrive here as `undefined`; this function never mints.
|
|
94
104
|
*/
|
|
95
105
|
const resolveDeviceKey = (
|
|
96
|
-
cfg: SessionStartConfig | undefined,
|
|
97
106
|
opts: UseSessionStartOptions,
|
|
107
|
+
autoDeviceKey: string | undefined,
|
|
98
108
|
): string | undefined => {
|
|
99
109
|
const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : undefined;
|
|
100
110
|
if (host) return host;
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
return autoDeviceKey;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the auto `device_key` for ONE app-open, then fire — the fix this hook never got.
|
|
116
|
+
*
|
|
117
|
+
* IT USED TO FIRE SYNCHRONOUSLY. `resolveAutoDeviceKey` is synchronous by contract: it returns a
|
|
118
|
+
* freshly minted id and adopts the persisted one a storage read later. So on a perfectly HEALTHY
|
|
119
|
+
* store this hook stamped a brand-new `wdev_*` on `app.session_started` every single launch,
|
|
120
|
+
* while every other kit surface adopted the persisted id milliseconds afterwards — the exact
|
|
121
|
+
* pre-fix behaviour the sibling hook's mount comment describes. `min_sessions` counts distinct
|
|
122
|
+
* opens grouped by that key, so it was structurally incapable of exceeding 1.
|
|
123
|
+
*
|
|
124
|
+
* The auto path now awaits `hydrateDeviceIdentity` (the provenance-carrying read) and honours the
|
|
125
|
+
* rule `context/deviceId.ts` states for exactly these callers: *"Callers that write a key onto the
|
|
126
|
+
* wire as a cross-launch join must read `durable` and refuse a `false`."* A host-supplied key or a
|
|
127
|
+
* config with no `storage` still fires synchronously — there is nothing to read.
|
|
128
|
+
*/
|
|
129
|
+
const openAutoDeviceKey = (fireOpen: (autoDeviceKey: string | undefined) => void): void => {
|
|
130
|
+
const { config: cfg, options: opts } = latest.current;
|
|
131
|
+
const hostKey =
|
|
132
|
+
typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : undefined;
|
|
133
|
+
if (hostKey || !cfg?.storage) {
|
|
134
|
+
fireOpen(undefined);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
|
|
138
|
+
if (cancelled) return;
|
|
139
|
+
fireOpen(identity?.durable ? identity.value : undefined);
|
|
140
|
+
});
|
|
103
141
|
};
|
|
104
142
|
|
|
105
|
-
const fire = () => {
|
|
143
|
+
const fire = (autoDeviceKey: string | undefined) => {
|
|
106
144
|
const { config: cfg, options: opts } = latest.current;
|
|
107
145
|
if (!cfg?.serverUrl) return;
|
|
108
146
|
if (opts.enabled === false) return;
|
|
@@ -114,7 +152,7 @@ export const useSessionStart = (
|
|
|
114
152
|
target,
|
|
115
153
|
// A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
|
|
116
154
|
userId: opts.userId,
|
|
117
|
-
deviceKey: resolveDeviceKey(
|
|
155
|
+
deviceKey: resolveDeviceKey(opts, autoDeviceKey),
|
|
118
156
|
sessionCount: opts.sessionCount,
|
|
119
157
|
appVersion: cfg.appVersion ?? device.appVersion,
|
|
120
158
|
platform: Platform.OS,
|
|
@@ -124,7 +162,7 @@ export const useSessionStart = (
|
|
|
124
162
|
};
|
|
125
163
|
|
|
126
164
|
// 1) Mount = an app-open.
|
|
127
|
-
fire
|
|
165
|
+
openAutoDeviceKey(fire);
|
|
128
166
|
|
|
129
167
|
// 2) Foreground after a real background = a new app-open.
|
|
130
168
|
let backgroundedAt: number | null = null;
|
|
@@ -137,11 +175,15 @@ export const useSessionStart = (
|
|
|
137
175
|
if (state === "active") {
|
|
138
176
|
const since = backgroundedAt;
|
|
139
177
|
backgroundedAt = null;
|
|
140
|
-
|
|
178
|
+
// A new open re-runs the same vetting: the auto key must still be a durable one, and a
|
|
179
|
+
// degraded read that released its latches gets a fresh attempt rather than a cached verdict.
|
|
180
|
+
if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) openAutoDeviceKey(fire);
|
|
141
181
|
}
|
|
142
182
|
};
|
|
143
183
|
const sub = AppState.addEventListener("change", onChange);
|
|
144
184
|
return () => {
|
|
185
|
+
// A pending hydration must not fire an app-open for a mount that is already gone.
|
|
186
|
+
cancelled = true;
|
|
145
187
|
// RN >= 0.65 returns a subscription with remove(); guard for older shims.
|
|
146
188
|
if (sub && typeof (sub as { remove?: () => void }).remove === "function") sub.remove();
|
|
147
189
|
};
|
package/src/types.ts
CHANGED
|
@@ -54,10 +54,11 @@ export type OnboardingResult = {
|
|
|
54
54
|
/** The raw message thread, for custom downstream parsing. */
|
|
55
55
|
raw: Message[];
|
|
56
56
|
/**
|
|
57
|
-
* The backend's onboarding plan, when
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* byte-identically what it
|
|
57
|
+
* The backend's onboarding plan, present when the backend sent one — the kit never infers it
|
|
58
|
+
* from which flow ran. Whether a plan arrives is the backend's configuration, not the kit's. Any
|
|
59
|
+
* run the server finished without a plan leaves this `undefined` AND leaves the key off the
|
|
60
|
+
* result object entirely — so a host written before plans existed sees byte-identically what it
|
|
61
|
+
* always saw.
|
|
61
62
|
*
|
|
62
63
|
* ⚠️ The kit does NOT interpret this and does NOT validate it. It checks one structural fact (a
|
|
63
64
|
* plan is an object) and hands the payload straight through, unread, unlogged, and never attached
|
|
@@ -240,10 +241,19 @@ export type WireOnboardingProps = {
|
|
|
240
241
|
*
|
|
241
242
|
* ```tsx
|
|
242
243
|
* <WireOnboarding userContext={activationJoinContext(deviceKey)} ... />
|
|
243
|
-
* // no device id of your own?
|
|
244
|
-
*
|
|
244
|
+
* // no device id of your own? pass `storage` and leave this prop alone — the kit injects its
|
|
245
|
+
* // own key, and only after it has confirmed the key actually persists (see `autoJoinKey`).
|
|
246
|
+
* <WireOnboarding config={{ ...config, storage }} ... />
|
|
245
247
|
* ```
|
|
246
248
|
*
|
|
249
|
+
* ⛔ Do NOT hand-build the auto key with `activationJoinContext(resolveAutoDeviceKey({...}))`.
|
|
250
|
+
* `resolveAutoDeviceKey` is SYNCHRONOUS by contract: it hands back a freshly minted id and adopts
|
|
251
|
+
* the persisted one a storage read later, and it cannot tell you whether the id survives the
|
|
252
|
+
* launch at all. A key that differs on every launch corrupts `min_sessions` rather than merely
|
|
253
|
+
* leaving the join empty. The auto-join path does the awaited, durability-checked read for you;
|
|
254
|
+
* a host that genuinely wants the value in hand should await `hydrateDeviceIdentity` and refuse a
|
|
255
|
+
* `durable: false` record, which is exactly what the kit does internally.
|
|
256
|
+
*
|
|
247
257
|
* SINCE 0.12.2, leaving it out no longer silently empties the funnel: when you pass `storage` and
|
|
248
258
|
* this prop carries no `device_key`, the kit injects its OWN per-install key — the same one the
|
|
249
259
|
* analytics surfaces mint and persist — so the default wiring joins. Anything you DO pass wins
|
package/src/utils/readPlan.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* readPlan — lift the backend's onboarding plan off the message thread.
|
|
3
3
|
*
|
|
4
|
-
* WHY IT EXISTS.
|
|
5
|
-
* on: `{ kind: "onboarding_plan", plan: {...} }`, alongside the component envelope the
|
|
6
|
-
* already consumes. The kit carries that payload out through `OnboardingResult.plan` and
|
|
7
|
-
* there. It does NOT interpret it, does NOT validate its fields, does NOT log it and does NOT
|
|
4
|
+
* WHY IT EXISTS. When it sends a plan, the server appends a SECOND A2A DataPart to the turn it
|
|
5
|
+
* finishes on: `{ kind: "onboarding_plan", plan: {...} }`, alongside the component envelope the
|
|
6
|
+
* renderer already consumes. The kit carries that payload out through `OnboardingResult.plan` and
|
|
7
|
+
* stops there. It does NOT interpret it, does NOT validate its fields, does NOT log it and does NOT
|
|
8
8
|
* attach it to any event — the plan is user-derived content, and deciding what it MEANS is the
|
|
9
9
|
* host's job (the kit/host boundary in `ai_rules/context_map.md`: the kit ends at the completion
|
|
10
10
|
* CTA).
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* so a throw here would cost the user the completion of an onboarding they already finished. Every
|
|
20
20
|
* step below is a runtime-guarded read.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* WHICH RUNS CARRY A PLAN IS THE BACKEND'S CONFIGURATION, NOT THE KIT'S. This reader matches the
|
|
23
|
+
* `kind` MARKER below and never the flow that produced the turn, so it needs no knowledge of how
|
|
24
|
+
* the tenant is configured. A run that carries no plan yields `undefined`: unchanged, and fully
|
|
25
|
+
* supported.
|
|
23
26
|
*/
|
|
24
27
|
import type { Message } from "wireai-rn";
|
|
25
28
|
|