@wireai/activation 0.14.3 → 0.16.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.
Files changed (85) hide show
  1. package/AGENTS.md +96 -21
  2. package/CHANGELOG.md +780 -0
  3. package/INTEGRATION_PROMPT.md +61 -23
  4. package/README.md +110 -31
  5. package/dist/analytics/index.d.mts +35 -13
  6. package/dist/analytics/index.d.ts +35 -13
  7. package/dist/analytics/index.js +288 -127
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +288 -127
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/coachmarks/index.d.mts +15 -1
  12. package/dist/coachmarks/index.d.ts +15 -1
  13. package/dist/coachmarks/index.js +120 -22
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +120 -22
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-CUvTOchb.d.mts → currentSession-Bz7G6lno.d.mts} +35 -39
  18. package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-z-CZ55ad.d.ts} +35 -39
  19. package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
  20. package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
  21. package/dist/index.d.mts +5 -2
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.js +125 -36
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +125 -36
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/questionnaire/index.d.mts +3 -16
  28. package/dist/questionnaire/index.d.ts +3 -16
  29. package/dist/questionnaire/index.js +202 -46
  30. package/dist/questionnaire/index.js.map +1 -1
  31. package/dist/questionnaire/index.mjs +203 -47
  32. package/dist/questionnaire/index.mjs.map +1 -1
  33. package/dist/reviews/index.d.mts +10 -6
  34. package/dist/reviews/index.d.ts +10 -6
  35. package/dist/reviews/index.js +269 -52
  36. package/dist/reviews/index.js.map +1 -1
  37. package/dist/reviews/index.mjs +270 -53
  38. package/dist/reviews/index.mjs.map +1 -1
  39. package/dist/showcase/index.d.mts +1 -1
  40. package/dist/showcase/index.d.ts +1 -1
  41. package/dist/showcase/index.js +106 -20
  42. package/dist/showcase/index.js.map +1 -1
  43. package/dist/showcase/index.mjs +107 -21
  44. package/dist/showcase/index.mjs.map +1 -1
  45. package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
  46. package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
  47. package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
  48. package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
  49. package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
  50. package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
  51. package/llms.txt +9 -9
  52. package/package.json +6 -9
  53. package/src/WireOnboarding.tsx +4 -1
  54. package/src/analytics/currentSession.ts +141 -4
  55. package/src/analytics/index.ts +6 -1
  56. package/src/analytics/reportClientEvent.ts +19 -10
  57. package/src/analytics/useAnalytics.ts +74 -15
  58. package/src/analytics/wireDoctor.ts +152 -7
  59. package/src/coachmarks/CoachmarkProvider.tsx +26 -5
  60. package/src/coachmarks/runtime.ts +156 -2
  61. package/src/coachmarks/types.ts +7 -2
  62. package/src/coachmarks/useCoachmarkTour.ts +51 -1
  63. package/src/context/deviceId.ts +72 -6
  64. package/src/features/WireFeaturesProvider.tsx +72 -12
  65. package/src/features/fetchWireFeatures.ts +49 -11
  66. package/src/features/useWireFeatures.ts +39 -3
  67. package/src/identity/identityRecord.ts +15 -2
  68. package/src/questionnaire/QuestionnaireGate.tsx +40 -1
  69. package/src/questionnaire/transport.ts +22 -8
  70. package/src/questionnaire/useQuestionnaireGate.ts +58 -7
  71. package/src/reviews/ReviewGate.tsx +125 -37
  72. package/src/reviews/decision.ts +11 -1
  73. package/src/reviews/idempotency.ts +109 -0
  74. package/src/reviews/index.ts +4 -2
  75. package/src/reviews/runtime.ts +44 -13
  76. package/src/reviews/transport.ts +39 -20
  77. package/src/reviews/types.ts +7 -0
  78. package/src/reviews/useReviewGate.ts +57 -7
  79. package/src/session-analytics/lifecycle.ts +16 -0
  80. package/src/session-analytics/useLifecycleEvents.ts +30 -2
  81. package/src/session-analytics/useSessionStart.ts +22 -2
  82. package/src/showcase/FeatureShowcase.tsx +50 -3
  83. package/src/types.ts +15 -8
  84. package/src/utils/submitResult.ts +17 -9
  85. package/src/utils/withDeadline.ts +70 -0
@@ -1,10 +1,10 @@
1
- export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-B_0SgCBe.js';
2
- import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-CW_5Mq4O.js';
3
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-CW_5Mq4O.js';
1
+ export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-xqqSFqxs.js';
2
+ import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-z-CZ55ad.js';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-z-CZ55ad.js';
4
4
  import { W as WireOnboardingStorage } from '../types-BpwiRpA8.js';
5
- import '../types-h2BZvl1t.js';
5
+ import '../types-tdATL5z0.js';
6
6
  import '../types-BKfpdZzX.js';
7
- import '../types-BcmagF6K.js';
7
+ import '../types-D_0B0yay.js';
8
8
  import 'react';
9
9
  import 'wireai-rn';
10
10
  import 'react-native';
@@ -105,13 +105,18 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
105
105
  * numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you
106
106
  * discover in a funnel report weeks later into something the first run tells you.
107
107
  *
108
- * SHAPE: four independent checks, each a `{name, ok, detail}` unit that can be read (and tested)
108
+ * SHAPE: five independent checks, each a `{name, ok, detail}` unit that can be read (and tested)
109
109
  * without the others. The report is data, never a thrown error and never a side effect on the host:
110
110
  *
111
111
  * 1. `target` : is there a server URL and a key, and do they look like a key and a URL?
112
112
  * 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)
113
113
  * 3. `storage` : can the offline queue persist? (a write / read / delete probe)
114
114
  * 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)
115
+ * 5. `join_key` : will those events be JOINABLE? (read-only; needs `join`)
116
+ *
117
+ * Checks 1-4 all answer "does an event leave and get accepted". NONE of them answers "can it be
118
+ * joined", and an integration with no `user_context.device_key` writes every event, passes every
119
+ * one of those four, and still reports a permanent ZERO in the activated funnel. That is check 5.
115
120
  *
116
121
  * NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor
117
122
  * that can crash the screen it is diagnosing is worse than no doctor.
@@ -131,6 +136,9 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
131
136
  * const report = await wireDoctor({
132
137
  * target: { serverUrl: "https://api.example.com", apiKey: DRIVELINE_KEY },
133
138
  * storage: AsyncStorage,
139
+ * // The same values you hand <WireOnboarding>. Omit `join` and the join_key check FAILS,
140
+ * // because a report that never looked at the join cannot honestly read green.
141
+ * join: { appId: WIRE_APP_ID, userContext },
134
142
  * });
135
143
  * console.log(report.ok, report.checks);
136
144
  */
@@ -138,7 +146,8 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
138
146
  /** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */
139
147
  type WireDoctorCheck = {
140
148
  /**
141
- * Stable id: `target` | `reachability` | `storage` | `round_trip` | `dev_only` | `internal_error`.
149
+ * Stable id: `target` | `reachability` | `storage` | `round_trip` | `join_key` | `dev_only` |
150
+ * `internal_error`.
142
151
  *
143
152
  * `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.
144
153
  * `internal_error` is the separate catch-all for a failure that got past every check's own
@@ -155,6 +164,19 @@ type WireDoctorReport = {
155
164
  ok: boolean;
156
165
  checks: WireDoctorCheck[];
157
166
  };
167
+ /**
168
+ * What the join check needs: the same three values the host hands `<WireOnboarding>`. Pass the very
169
+ * props you pass the component — anything reconstructed here would diagnose a DIFFERENT integration
170
+ * than the one that ships.
171
+ */
172
+ type WireDoctorJoinTarget = {
173
+ /** The same `config.appId`. It namespaces the persisted auto key, so the wrong one reads the wrong slot. */
174
+ appId?: string;
175
+ /** The same `userContext` prop. Only `device_key` is read; nothing else is inspected or reported. */
176
+ userContext?: Record<string, string | number | boolean>;
177
+ /** The same `autoJoinKey` prop. `false` is the documented opt-out from the kit's auto-join. */
178
+ autoJoinKey?: boolean;
179
+ };
158
180
  /** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */
159
181
  type WireDoctorOptions = {
160
182
  /** The same `{serverUrl, apiKey}` the kit is configured with. */
@@ -164,6 +186,11 @@ type WireDoctorOptions = {
164
186
  * check reports the DEGRADED in-memory mode rather than failing.
165
187
  */
166
188
  storage?: WireOnboardingStorage;
189
+ /**
190
+ * The join-key inputs. OMITTING THIS FAILS THE `join_key` CHECK — deliberately, and see
191
+ * {@link checkJoinKey} for why a skipped join check may not read as a pass.
192
+ */
193
+ join?: WireDoctorJoinTarget;
167
194
  };
168
195
  /**
169
196
  * Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.
@@ -283,11 +310,6 @@ type Analytics = {
283
310
  */
284
311
  declare const createAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
285
312
 
286
- /**
287
- * Build a per-mount analytics instance. `config`/`options` are read once at first render (the
288
- * instance is stable for the component's lifetime, held in a ref). Returns the {@link Analytics}
289
- * surface so the component can `track` / `screen` / `identify` and drive `notifyOnline` on reconnect.
290
- */
291
313
  declare const useAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
292
314
 
293
- export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
315
+ export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorJoinTarget, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
@@ -10,6 +10,28 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  throw Error('Dynamic require of "' + x + '" is not supported');
11
11
  });
12
12
 
13
+ // src/utils/withDeadline.ts
14
+ var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
15
+ var DEFAULT_DEADLINE_MS = 15e3;
16
+ var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
17
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
18
+ let timer;
19
+ const expired = new Promise((resolve) => {
20
+ timer = setTimeout(() => {
21
+ controller == null ? void 0 : controller.abort();
22
+ resolve(DEADLINE_EXPIRED);
23
+ }, timeoutMs);
24
+ });
25
+ const running = exchange(controller == null ? void 0 : controller.signal);
26
+ try {
27
+ return await Promise.race([running, expired]);
28
+ } finally {
29
+ clearTimeout(timer);
30
+ void running.catch(() => {
31
+ });
32
+ }
33
+ };
34
+
13
35
  // src/analytics/reportClientEvent.ts
14
36
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
15
37
  var toWireEvents = (events) => events.map((event) => {
@@ -81,14 +103,17 @@ var reportClientEventsOutcome = async (target, events) => {
81
103
  try {
82
104
  const req = buildEventsRequest(target, events);
83
105
  if (!req) return "refused";
84
- const res = await fetch(req.url, req.init);
85
- if (!res || !res.ok) return "unreachable";
86
- const ack = await readEventsAck(res);
87
- if (!ack || ack.skipped <= 0) return "delivered";
88
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
89
- console.warn(describeDiscarded(ack));
90
- }
91
- return "refused";
106
+ const outcome = await withDeadline(async (signal) => {
107
+ const res = await fetch(req.url, { ...req.init, signal });
108
+ if (!res || !res.ok) return "unreachable";
109
+ const ack = await readEventsAck(res);
110
+ if (!ack || ack.skipped <= 0) return "delivered";
111
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
112
+ console.warn(describeDiscarded(ack));
113
+ }
114
+ return "refused";
115
+ });
116
+ return outcome === DEADLINE_EXPIRED ? "unreachable" : outcome;
92
117
  } catch {
93
118
  return "unreachable";
94
119
  }
@@ -109,25 +134,51 @@ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
109
134
  "@wireai/activation:currentSessionId"
110
135
  );
111
136
  var globalSlot = globalThis;
137
+ var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
138
+ "@wireai/activation:currentSessionIdListeners"
139
+ );
140
+ var listenerSlot = globalThis;
141
+ var listeners = () => {
142
+ const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
143
+ if (existing) return existing;
144
+ const created = /* @__PURE__ */ new Set();
145
+ listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
146
+ return created;
147
+ };
148
+ var writeCurrentSessionId = (id) => {
149
+ if (globalSlot[CURRENT_SESSION_ID_SLOT] === id) return;
150
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
151
+ for (const listener of Array.from(listeners())) {
152
+ try {
153
+ listener();
154
+ } catch {
155
+ }
156
+ }
157
+ };
112
158
  var setCurrentSessionId = (id) => {
113
159
  if (typeof id === "string" && id.length > 0) {
114
- globalSlot[CURRENT_SESSION_ID_SLOT] = id;
160
+ writeCurrentSessionId(id);
115
161
  }
116
162
  };
117
163
  var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
118
164
  var resetCurrentSessionId = () => {
119
- globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
165
+ writeCurrentSessionId(void 0);
120
166
  };
121
167
  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.";
122
168
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
123
169
  "@wireai/activation:currentSessionIdMintWarned"
124
170
  );
125
171
  var warnSlot = globalThis;
172
+ var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
173
+ "@wireai/activation:mintedSessionId"
174
+ );
175
+ var mintedSlot = globalThis;
126
176
  var ensureCurrentSessionId = () => {
127
177
  const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
128
178
  if (typeof existing === "string" && existing.length > 0) return existing;
129
179
  const minted = makeSessionId();
130
- globalSlot[CURRENT_SESSION_ID_SLOT] = minted;
180
+ writeCurrentSessionId(minted);
181
+ mintedSlot[MINTED_SESSION_ID_SLOT] = minted;
131
182
  if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
132
183
  warnSlot[MINT_WARNED_SLOT] = true;
133
184
  }
@@ -228,6 +279,109 @@ var useScreenTracking = (navigationRef, options = {}) => {
228
279
  }, [navigationRef]);
229
280
  };
230
281
 
282
+ // src/identity/identityRecord.ts
283
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
284
+ var provenanceGlobal = globalThis;
285
+ var provenanceRegistry = () => {
286
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
287
+ if (existing) return existing;
288
+ const created = { host: /* @__PURE__ */ new Map() };
289
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
290
+ return created;
291
+ };
292
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
293
+ var isUsableIdentityValue = (value) => typeof value === "string" && value.trim().length > 0;
294
+ var resolveIdentity = (input) => {
295
+ var _a;
296
+ if (!isUsableIdentityValue(input.value)) return void 0;
297
+ const value = input.value.trim();
298
+ const durable = (_a = input.durable) != null ? _a : input.source === "host";
299
+ {
300
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
301
+ }
302
+ return { value, space: input.space, source: input.source, durable };
303
+ };
304
+
305
+ // src/context/deviceId.ts
306
+ var AUTO_DEVICE_ID_PREFIX = "wdev_";
307
+ var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
308
+ var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
309
+ var mintDeviceId = () => {
310
+ const time = Date.now().toString(36);
311
+ return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
312
+ };
313
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
314
+ var deviceKeyGlobal = globalThis;
315
+ var autoDeviceKeyRegistry = () => {
316
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
317
+ if (existing) return existing;
318
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
319
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
320
+ return created;
321
+ };
322
+ var startHydration = (registry, appId, storage, minted) => {
323
+ if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
324
+ const existing = registry.pending.get(appId);
325
+ if (existing) return existing;
326
+ const slot = deviceIdStorageKey(appId);
327
+ const degraded = () => {
328
+ var _a;
329
+ return { value: (_a = registry.keys.get(appId)) != null ? _a : minted, durable: false };
330
+ };
331
+ const adopted = (value) => ({ value, durable: true });
332
+ let run;
333
+ try {
334
+ run = Promise.resolve(storage.getItem(slot)).then((saved) => {
335
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
336
+ if (persisted) {
337
+ registry.keys.set(appId, persisted);
338
+ return adopted(persisted);
339
+ }
340
+ return Promise.resolve(storage.setItem(slot, minted)).then(
341
+ () => {
342
+ var _a;
343
+ return adopted((_a = registry.keys.get(appId)) != null ? _a : minted);
344
+ },
345
+ degraded
346
+ );
347
+ }).catch(degraded);
348
+ } catch {
349
+ run = Promise.resolve(degraded());
350
+ }
351
+ registry.pending.set(appId, run);
352
+ void run.then((outcome) => {
353
+ var _a;
354
+ if (outcome.durable) return;
355
+ (_a = registry.pending) == null ? void 0 : _a.delete(appId);
356
+ registry.hydrating.delete(appId);
357
+ });
358
+ return run;
359
+ };
360
+ var resolveAutoDeviceKey = (opts = {}) => {
361
+ var _a, _b;
362
+ const registry = autoDeviceKeyRegistry();
363
+ const appId = (_a = opts.appId) != null ? _a : "default";
364
+ let id = registry.keys.get(appId);
365
+ if (!id) {
366
+ id = mintDeviceId();
367
+ registry.keys.set(appId, id);
368
+ }
369
+ const storage = opts.storage;
370
+ if (storage && !registry.hydrating.has(appId)) {
371
+ registry.hydrating.add(appId);
372
+ void startHydration(registry, appId, storage, id);
373
+ }
374
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
375
+ };
376
+ var resetAutoDeviceKeys = () => {
377
+ var _a;
378
+ const registry = autoDeviceKeyRegistry();
379
+ registry.keys.clear();
380
+ registry.hydrating.clear();
381
+ (_a = registry.pending) == null ? void 0 : _a.clear();
382
+ registry.ambient = void 0;
383
+ };
384
+
231
385
  // src/analytics/wireDoctor.ts
232
386
  var PROBE_STORAGE_KEY = "wireai:doctor:probe";
233
387
  var PROBE_QUESTION_KEY = "wire_doctor_probe";
@@ -358,6 +512,66 @@ var checkRoundTrip = async (target) => {
358
512
  `the server DISCARDED the probe event: written=${(_a = ack.written) != null ? _a : "unknown"}, skipped=${ack.skipped}` + (ack.reasons.length > 0 ? `, reasons: ${ack.reasons.join(", ")}.` : ". It gave no reason; check the server's ingest log for this request.")
359
513
  );
360
514
  };
515
+ var checkJoinKey = async (join, storage, storageOk) => {
516
+ var _a;
517
+ if (!join) {
518
+ return check(
519
+ "join_key",
520
+ false,
521
+ "NOT EVALUATED: pass `join: { appId, userContext, autoJoinKey }` \u2014 the same values you pass <WireOnboarding> \u2014 so the doctor can tell whether this integration's sessions will be joinable. It is reported as a failure rather than skipped because every other check can pass while the funnel reads a permanent zero."
522
+ );
523
+ }
524
+ if (isUsableIdentityValue((_a = join.userContext) == null ? void 0 : _a.device_key)) {
525
+ return check(
526
+ "join_key",
527
+ true,
528
+ "user_context.device_key is supplied by the host, so onboarding sessions join the rest of the funnel."
529
+ );
530
+ }
531
+ if (join.autoJoinKey === false) {
532
+ return check(
533
+ "join_key",
534
+ false,
535
+ "no user_context.device_key AND autoJoinKey is false, so these onboarding sessions are UNLINKED by choice: they join nothing the app reports later and the activated funnel reads zero. Supply user_context.device_key, or drop autoJoinKey:false to let the kit inject its own."
536
+ );
537
+ }
538
+ if (!storage) {
539
+ return check(
540
+ "join_key",
541
+ false,
542
+ "no user_context.device_key, and the kit cannot auto-inject one without storage: an unpersisted key differs every launch, which corrupts the server's session counting rather than just leaving it empty, so the kit declines it. Pass AsyncStorage (or an MMKV wrapper) to <WireOnboarding>."
543
+ );
544
+ }
545
+ if (!storageOk) {
546
+ return check(
547
+ "join_key",
548
+ false,
549
+ "no user_context.device_key, and the storage check above FAILED \u2014 the kit refuses a non-durable auto key, so it will inject nothing and these sessions will not join. Fix the storage adapter first."
550
+ );
551
+ }
552
+ let persisted = null;
553
+ try {
554
+ persisted = await storage.getItem(deviceIdStorageKey(join.appId));
555
+ } catch {
556
+ return check(
557
+ "join_key",
558
+ false,
559
+ "no user_context.device_key, and reading the persisted auto key threw, so it is unknown whether the kit can supply one. Treat this as the storage adapter being unreliable."
560
+ );
561
+ }
562
+ if (isUsableIdentityValue(persisted)) {
563
+ return check(
564
+ "join_key",
565
+ true,
566
+ "no host device_key, but a durable auto join key is already persisted for this appId, so sessions join."
567
+ );
568
+ }
569
+ return check(
570
+ "join_key",
571
+ true,
572
+ "no host device_key and none persisted yet (first run): the kit will mint and persist its own on first mount, and the storage check above proved the write/read round trip that injection is gated on."
573
+ );
574
+ };
361
575
  var wireDoctor = async (options) => {
362
576
  if (typeof __DEV__ === "undefined" || !__DEV__) {
363
577
  return {
@@ -373,14 +587,18 @@ var wireDoctor = async (options) => {
373
587
  }
374
588
  try {
375
589
  const target = options == null ? void 0 : options.target;
376
- const checks = [checkTarget(target)];
377
- if (checks[0].ok && target) {
590
+ const targetCheck = checkTarget(target);
591
+ const checks = [targetCheck];
592
+ const targetUsable = targetCheck.ok && !!target;
593
+ if (targetUsable && target) {
378
594
  checks.push(await checkReachability(target.serverUrl));
379
- checks.push(await checkStorage(options == null ? void 0 : options.storage));
595
+ }
596
+ const storageCheck = await checkStorage(options == null ? void 0 : options.storage);
597
+ checks.push(storageCheck);
598
+ if (targetUsable && target) {
380
599
  checks.push(await checkRoundTrip(target));
381
- } else {
382
- checks.push(await checkStorage(options == null ? void 0 : options.storage));
383
600
  }
601
+ checks.push(await checkJoinKey(options == null ? void 0 : options.join, options == null ? void 0 : options.storage, storageCheck.ok));
384
602
  return { ok: checks.every((c) => c.ok), checks };
385
603
  } catch {
386
604
  return {
@@ -1031,108 +1249,6 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
1031
1249
  return result;
1032
1250
  };
1033
1251
 
1034
- // src/identity/identityRecord.ts
1035
- var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
1036
- var provenanceGlobal = globalThis;
1037
- var provenanceRegistry = () => {
1038
- const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
1039
- if (existing) return existing;
1040
- const created = { host: /* @__PURE__ */ new Map() };
1041
- provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
1042
- return created;
1043
- };
1044
- var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
1045
- var resolveIdentity = (input) => {
1046
- var _a;
1047
- if (typeof input.value !== "string") return void 0;
1048
- const value = input.value.trim();
1049
- if (!value) return void 0;
1050
- const durable = (_a = input.durable) != null ? _a : input.source === "host";
1051
- {
1052
- provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
1053
- }
1054
- return { value, space: input.space, source: input.source, durable };
1055
- };
1056
-
1057
- // src/context/deviceId.ts
1058
- var AUTO_DEVICE_ID_PREFIX = "wdev_";
1059
- var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
1060
- var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
1061
- var mintDeviceId = () => {
1062
- const time = Date.now().toString(36);
1063
- return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
1064
- };
1065
- var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
1066
- var deviceKeyGlobal = globalThis;
1067
- var autoDeviceKeyRegistry = () => {
1068
- const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
1069
- if (existing) return existing;
1070
- const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
1071
- deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
1072
- return created;
1073
- };
1074
- var startHydration = (registry, appId, storage, minted) => {
1075
- if (!registry.pending) registry.pending = /* @__PURE__ */ new Map();
1076
- const existing = registry.pending.get(appId);
1077
- if (existing) return existing;
1078
- const slot = deviceIdStorageKey(appId);
1079
- const degraded = () => {
1080
- var _a;
1081
- return { value: (_a = registry.keys.get(appId)) != null ? _a : minted, durable: false };
1082
- };
1083
- const adopted = (value) => ({ value, durable: true });
1084
- let run;
1085
- try {
1086
- run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1087
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1088
- if (persisted) {
1089
- registry.keys.set(appId, persisted);
1090
- return adopted(persisted);
1091
- }
1092
- return Promise.resolve(storage.setItem(slot, minted)).then(
1093
- () => {
1094
- var _a;
1095
- return adopted((_a = registry.keys.get(appId)) != null ? _a : minted);
1096
- },
1097
- degraded
1098
- );
1099
- }).catch(degraded);
1100
- } catch {
1101
- run = Promise.resolve(degraded());
1102
- }
1103
- registry.pending.set(appId, run);
1104
- void run.then((outcome) => {
1105
- var _a;
1106
- if (outcome.durable) return;
1107
- (_a = registry.pending) == null ? void 0 : _a.delete(appId);
1108
- registry.hydrating.delete(appId);
1109
- });
1110
- return run;
1111
- };
1112
- var resolveAutoDeviceKey = (opts = {}) => {
1113
- var _a, _b;
1114
- const registry = autoDeviceKeyRegistry();
1115
- const appId = (_a = opts.appId) != null ? _a : "default";
1116
- let id = registry.keys.get(appId);
1117
- if (!id) {
1118
- id = mintDeviceId();
1119
- registry.keys.set(appId, id);
1120
- }
1121
- const storage = opts.storage;
1122
- if (storage && !registry.hydrating.has(appId)) {
1123
- registry.hydrating.add(appId);
1124
- void startHydration(registry, appId, storage, id);
1125
- }
1126
- return (_b = registry.keys.get(appId)) != null ? _b : id;
1127
- };
1128
- var resetAutoDeviceKeys = () => {
1129
- var _a;
1130
- const registry = autoDeviceKeyRegistry();
1131
- registry.keys.clear();
1132
- registry.hydrating.clear();
1133
- (_a = registry.pending) == null ? void 0 : _a.clear();
1134
- };
1135
-
1136
1252
  // src/analytics/analyticsFacade.ts
1137
1253
  var createAnalytics = (config, options = {}) => {
1138
1254
  var _a, _b, _c;
@@ -1285,30 +1401,75 @@ var createAnalytics = (config, options = {}) => {
1285
1401
  dispose: queue.dispose
1286
1402
  };
1287
1403
  };
1404
+ var stableAnalyticsFacade = (live) => ({
1405
+ track: (event, props) => {
1406
+ var _a;
1407
+ return (_a = live()) == null ? void 0 : _a.track(event, props);
1408
+ },
1409
+ screen: (name, props) => {
1410
+ var _a;
1411
+ return (_a = live()) == null ? void 0 : _a.screen(name, props);
1412
+ },
1413
+ identify: (userId, traits) => {
1414
+ var _a;
1415
+ return (_a = live()) == null ? void 0 : _a.identify(userId, traits);
1416
+ },
1417
+ setUserContext: (partial) => {
1418
+ var _a;
1419
+ return (_a = live()) == null ? void 0 : _a.setUserContext(partial);
1420
+ },
1421
+ reset: () => {
1422
+ var _a;
1423
+ return (_a = live()) == null ? void 0 : _a.reset();
1424
+ },
1425
+ flush: () => {
1426
+ var _a;
1427
+ return (_a = live()) == null ? void 0 : _a.flush();
1428
+ },
1429
+ notifyOnline: () => {
1430
+ var _a;
1431
+ return (_a = live()) == null ? void 0 : _a.notifyOnline();
1432
+ },
1433
+ // After a real unmount there is no instance and nothing pending on it — 0 is the honest answer.
1434
+ size: () => {
1435
+ var _a, _b;
1436
+ return (_b = (_a = live()) == null ? void 0 : _a.size()) != null ? _b : 0;
1437
+ },
1438
+ dispose: () => {
1439
+ var _a;
1440
+ return (_a = live()) == null ? void 0 : _a.dispose();
1441
+ }
1442
+ });
1288
1443
  var useAnalytics = (config, options = {}) => {
1289
1444
  var _a, _b;
1290
1445
  const ref = react.useRef(void 0);
1291
1446
  const prevKeys = react.useRef("");
1447
+ const inputs = react.useRef({ config, options });
1448
+ inputs.current = { config, options };
1292
1449
  const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
1293
1450
  if (!ref.current || prevKeys.current !== currentKeys) {
1294
1451
  prevKeys.current = currentKeys;
1295
1452
  (_a = ref.current) == null ? void 0 : _a.dispose();
1296
1453
  ref.current = createAnalytics(config, options);
1297
1454
  }
1455
+ const facade = react.useRef(void 0);
1456
+ if (!facade.current) facade.current = stableAnalyticsFacade(() => ref.current);
1457
+ react.useEffect(() => {
1458
+ if (!ref.current) {
1459
+ ref.current = createAnalytics(inputs.current.config, inputs.current.options);
1460
+ }
1461
+ return () => {
1462
+ var _a2;
1463
+ (_a2 = ref.current) == null ? void 0 : _a2.dispose();
1464
+ ref.current = void 0;
1465
+ };
1466
+ }, []);
1298
1467
  const hostDeviceKey = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1299
1468
  react.useEffect(() => {
1300
1469
  var _a2;
1301
1470
  if (hostDeviceKey) (_a2 = ref.current) == null ? void 0 : _a2.setUserContext({ deviceKey: hostDeviceKey });
1302
1471
  }, [hostDeviceKey]);
1303
- react.useEffect(
1304
- () => () => {
1305
- var _a2;
1306
- (_a2 = ref.current) == null ? void 0 : _a2.dispose();
1307
- ref.current = void 0;
1308
- },
1309
- []
1310
- );
1311
- return ref.current;
1472
+ return facade.current;
1312
1473
  };
1313
1474
 
1314
1475
  exports.AUTO_DEVICE_ID_PREFIX = AUTO_DEVICE_ID_PREFIX;