@wireai/activation 0.3.0 → 0.7.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 (75) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +57 -0
  3. package/dist/analytics/index.d.mts +15 -2
  4. package/dist/analytics/index.d.ts +15 -2
  5. package/dist/analytics/index.js +224 -14
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs +222 -15
  8. package/dist/analytics/index.mjs.map +1 -1
  9. package/dist/coachmarks/index.js +6 -1
  10. package/dist/coachmarks/index.js.map +1 -1
  11. package/dist/coachmarks/index.mjs +6 -1
  12. package/dist/coachmarks/index.mjs.map +1 -1
  13. package/dist/{eventQueue-CA1d8Fmn.d.mts → currentSession-BJBB7i4-.d.mts} +143 -15
  14. package/dist/{eventQueue-CrNB9gzH.d.ts → currentSession-CxnP7gAa.d.ts} +143 -15
  15. package/dist/index.d.mts +40 -39
  16. package/dist/index.d.ts +40 -39
  17. package/dist/index.js +378 -154
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +370 -156
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/questionnaire/index.d.mts +1 -1
  22. package/dist/questionnaire/index.d.ts +1 -1
  23. package/dist/questionnaire/index.js +29 -5
  24. package/dist/questionnaire/index.js.map +1 -1
  25. package/dist/questionnaire/index.mjs +30 -6
  26. package/dist/questionnaire/index.mjs.map +1 -1
  27. package/dist/reviews/index.d.mts +1 -1
  28. package/dist/reviews/index.d.ts +1 -1
  29. package/dist/reviews/index.js +40 -9
  30. package/dist/reviews/index.js.map +1 -1
  31. package/dist/reviews/index.mjs +42 -11
  32. package/dist/reviews/index.mjs.map +1 -1
  33. package/dist/showcase/index.d.mts +1 -14
  34. package/dist/showcase/index.d.ts +1 -14
  35. package/dist/showcase/index.js +8 -2
  36. package/dist/showcase/index.js.map +1 -1
  37. package/dist/showcase/index.mjs +8 -2
  38. package/dist/showcase/index.mjs.map +1 -1
  39. package/package.json +3 -2
  40. package/src/OnboardingFlow.tsx +8 -4
  41. package/src/WireOnboarding.tsx +12 -6
  42. package/src/analytics/analyticsFacade.ts +86 -10
  43. package/src/analytics/contextEnvelope.ts +13 -7
  44. package/src/analytics/currentSession.ts +35 -0
  45. package/src/analytics/eventQueue.ts +5 -0
  46. package/src/analytics/index.ts +3 -0
  47. package/src/analytics/useAnalytics.ts +7 -1
  48. package/src/analytics/useScreenTracking.ts +23 -1
  49. package/src/cards/InterstitialCard.tsx +1 -1
  50. package/src/cards/NumberStepperCard.tsx +12 -7
  51. package/src/cards/StatusCard.tsx +13 -8
  52. package/src/cards/TextInputCard.tsx +10 -5
  53. package/src/components/AnimatedSparkle.tsx +20 -3
  54. package/src/components/Button.tsx +10 -1
  55. package/src/components/CardHandoff.tsx +2 -6
  56. package/src/components/CardLayout.tsx +16 -10
  57. package/src/components/Illustration.tsx +9 -5
  58. package/src/components/LoadingBlock.tsx +44 -29
  59. package/src/components/LoadingScreen.tsx +3 -2
  60. package/src/components/OnboardingScaffold.tsx +21 -23
  61. package/src/context/userContext.ts +210 -0
  62. package/src/device/appVersion.ts +103 -0
  63. package/src/device/deviceContext.ts +17 -5
  64. package/src/features/WireFeaturesProvider.tsx +7 -2
  65. package/src/identity/userIdentity.ts +5 -0
  66. package/src/index.ts +23 -0
  67. package/src/questionnaire/QuestionnaireGate.tsx +15 -3
  68. package/src/reviews/ReviewGate.tsx +27 -7
  69. package/src/reviews/ReviewModal.tsx +4 -0
  70. package/src/session-analytics/reportSessionStart.ts +7 -0
  71. package/src/session-analytics/useLifecycleEvents.ts +4 -2
  72. package/src/session-analytics/useSessionStart.ts +2 -1
  73. package/src/showcase/FeatureShowcase.tsx +2 -1
  74. package/src/types.ts +6 -5
  75. package/src/components/loaderChrome.ts +0 -28
package/AGENTS.md CHANGED
@@ -78,7 +78,7 @@ There is no `analytics` subpath: the app-event / analytics API is exported from
78
78
  | `serverUrl` | `string` | yes | Base server URL; the kit appends `/a2a`. |
79
79
  | `appId` | `string` | yes | Passed as the A2A `model` (informational; the key resolves the app). |
80
80
  | `metadata` | `Record<string, unknown>` | no | Merged into every A2A request (e.g. install attribution). The kit reserves `sessionId` + `supportedComponents`; do not override them. |
81
- | `appVersion` | `string` | no | Host app version string (e.g. `"1.4.2"`); forwarded to the backend + client events so analytics can segment the funnel by app version. |
81
+ | `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). |
82
82
 
83
83
  ## App events / analytics (root export)
84
84
 
package/CHANGELOG.md CHANGED
@@ -20,6 +20,63 @@ Historical entries below the rename keep the old package name on purpose.
20
20
 
21
21
  ## [0.7.0] — Unreleased
22
22
 
23
+ ### Added: `WireUserContext` — one rich user-context object → every event's `user_context`
24
+
25
+ - New `context/userContext.ts` + public export `WireUserContext`: a single, extensible object a host
26
+ passes ONCE (`appVersion?`, `deviceKey?`, `userId?`, `userEmail?` + `hashEmail?`, `extra?`) that the
27
+ kit flows into every analytics event. Pure `resolveUserContext(ctx, { autoAppVersion })` merges it
28
+ with ONE precedence rule — an explicit field WINS over the auto-detected `device`/`appVersion` —
29
+ and omits missing fields (never sent empty). Also exported: `namespaceExtra`, `hashEmailFnv1a`,
30
+ `isWireScalar`, `RESERVED_USER_CONTEXT_KEYS`, `EXTRA_KEY_PREFIX`.
31
+ - Deliberate bucket separation (nothing leaks): `userId` → the event's TOP-LEVEL opaque `user_id`
32
+ (via `sanitizeUserId`, capped 128) and NEVER the `user_context` bucket; `deviceKey` →
33
+ `user_context.device_key` (NOT `session_id`); `appVersion` → `user_context.app_version`; `extra`
34
+ entries are coerced to `string | number | boolean` (non-scalars/NaN/Infinity dropped) and
35
+ NAMESPACED under a `custom.` key prefix so a host extra can never collide with a reserved key.
36
+ - **`userEmail` is OPT-IN PII in its OWN field** (`user_context.user_email`) — never merged into
37
+ `userId`. The kit NEVER auto-collects it; a host passes it only with the user's consent (EU users:
38
+ personal data). Set `hashEmail: true` to send a dependency-free FNV-1a fold instead of the raw
39
+ address (`user_context.user_email_hashed: true`). NOTE: FNV-1a is a lightweight non-cryptographic
40
+ fold; for a cryptographic digest, pre-hash host-side (e.g. `expo-crypto` SHA-256) and pass that as
41
+ `userEmail` with `hashEmail` falsy. Documented in the field's TSDoc.
42
+ - The analytics façade (`createAnalytics`) now accepts `userContext` at init AND exposes
43
+ `setUserContext(partial)` so a host can attach `userId`/`userEmail` at LOGIN without remounting;
44
+ both flow into subsequent `track`/`screen`/`identify` events' `user_context`. Backward compatible:
45
+ `createAnalytics({ serverUrl, apiKey })` with no `userContext` behaves exactly as before.
46
+
47
+ ### Fixed: `identify` + host app-events reuse the live per-open session (kills the phantom-session)
48
+
49
+ - New `analytics/currentSession.ts` (`getCurrentSessionId` / `setCurrentSessionId` /
50
+ `resetCurrentSessionId`): a process-local registry of the CURRENT per-open `session_id`.
51
+ `reportSessionStart` registers each per-open id here on every app-open.
52
+ - The façade's `identify` (and `track`/`screen` app-events) now source their `session_id` from
53
+ `config.sessionId ?? getCurrentSessionId() ?? <stable per-instance id>` — so identity binds to the
54
+ session the server ALREADY saw (via `app.session_started`) instead of minting a fresh id the server
55
+ back-fills into a synthetic session. This is what inflated the Morrow/Myelino session counts. An
56
+ explicit `config.sessionId` still freezes the id (opt-out). This is also the kit half of the #205
57
+ "align all app-events to the per-open session" follow-up. **No server change required** for the
58
+ normal flow (session-start fires on mount, identify at login); the only residual is an `identify`
59
+ fired before any open this launch, which falls back to the instance id.
60
+ - `identifyOnboarding` gains a last-resort fallback to `getCurrentSessionId()` when no `contextId`
61
+ is captured or resolvable from storage, so a post-flow identify binds to the live open session
62
+ instead of no-oping. An explicit `contextId` (or a stored one) still wins.
63
+
64
+ ### Added: `device.appVersion` auto-detection — populate the `by_app_version` breakdown by default
65
+
66
+ - `collectDeviceContext()` now carries a best-effort `appVersion` so the analytics
67
+ `by_app_version` funnel breakdown works out of the box, instead of staying empty until a host
68
+ remembers to pass `config.appVersion`. New `device/appVersion.ts` reads the version from
69
+ `expo-constants` (`expoConfig.version`, then `nativeAppVersion`) or `expo-application`
70
+ (`nativeApplicationVersion`) when present.
71
+ - ADDS NO DEPENDENCY: the Expo modules are read through a guarded, variable-specifier `require`,
72
+ so a host WITHOUT them (e.g. bare React Native) never fails to bundle — the read simply yields
73
+ `undefined`. Nothing is added to `package.json`; no peer is forced.
74
+ - An explicit host `config.appVersion` still WINS everywhere (the `WireOnboarding` device memo, the
75
+ `useSessionStart` / `useLifecycleEvents` hooks, and the analytics context envelope overwrite the
76
+ auto-detected value with the host's). The context envelope also mirrors the effective version
77
+ onto its outer `appVersion` scalar so `user_context.app_version` populates on facade events too.
78
+ - New public export `detectAppVersion()` (alongside `collectDeviceContext`). Never throws.
79
+
23
80
  ### Added: session mapping — know when a user opens the app again
24
81
 
25
82
  - New `session-analytics/` module. `reportSessionStart(opts)` posts ONE standard
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-BeO_Brcu.mjs';
2
- import { E as EventQueueOptions } from '../eventQueue-CA1d8Fmn.mjs';
3
- export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, W as WIRE_ONBOARDING_EVENTS, g as WireOnboardingEventName, h as buildContextEnvelope, i as createEventQueue, m as makeSessionId, r as reportClientEvent, j as reportClientEvents, t as toAnalyticsEvent } from '../eventQueue-CA1d8Fmn.mjs';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-BJBB7i4-.mjs';
3
+ export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, g as WIRE_ONBOARDING_EVENTS, h as WireOnboardingEventName, i as buildContextEnvelope, j as createEventQueue, k as getCurrentSessionId, m as makeSessionId, r as reportClientEvent, l as reportClientEvents, n as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-BJBB7i4-.mjs';
4
4
  import '../types-A6pTxIZV.mjs';
5
5
  import '../types-BKfpdZzX.mjs';
6
6
  import '../types-GL_hQ0TN.mjs';
@@ -120,6 +120,13 @@ type CreateAnalyticsConfig = {
120
120
  appBuild?: string;
121
121
  /** Host connectivity signal, e.g. "wifi" | "cellular" — read fresh per event via the provider. */
122
122
  networkType?: string;
123
+ /**
124
+ * The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque
125
+ * user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via
126
+ * {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional — omit it
127
+ * and events carry only the device context, exactly as before.
128
+ */
129
+ userContext?: WireUserContext;
123
130
  };
124
131
  /** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
125
132
  type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
@@ -135,6 +142,12 @@ type Analytics = {
135
142
  screen(name: string, props?: AnalyticsProps): void;
136
143
  /** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
137
144
  identify(userId: string, traits?: AnalyticsProps): void;
145
+ /**
146
+ * Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
147
+ * merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
148
+ * binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
149
+ */
150
+ setUserContext(partial: Partial<WireUserContext>): void;
138
151
  /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
139
152
  flush(): void;
140
153
  /** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DLpd1v5_.js';
2
- import { E as EventQueueOptions } from '../eventQueue-CrNB9gzH.js';
3
- export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, W as WIRE_ONBOARDING_EVENTS, g as WireOnboardingEventName, h as buildContextEnvelope, i as createEventQueue, m as makeSessionId, r as reportClientEvent, j as reportClientEvents, t as toAnalyticsEvent } from '../eventQueue-CrNB9gzH.js';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-CxnP7gAa.js';
3
+ export { A as AnalyticsEvent, C as ClientEvent, a as ClientEventTarget, b as ClientEventType, c as ContextEnvelope, d as ContextEnvelopeInput, e as EnvelopeSource, f as EventQueue, g as WIRE_ONBOARDING_EVENTS, h as WireOnboardingEventName, i as buildContextEnvelope, j as createEventQueue, k as getCurrentSessionId, m as makeSessionId, r as reportClientEvent, l as reportClientEvents, n as resetCurrentSessionId, s as setCurrentSessionId, t as toAnalyticsEvent } from '../currentSession-CxnP7gAa.js';
4
4
  import '../types-BhpXJGlg.js';
5
5
  import '../types-BKfpdZzX.js';
6
6
  import '../types-GL_hQ0TN.js';
@@ -120,6 +120,13 @@ type CreateAnalyticsConfig = {
120
120
  appBuild?: string;
121
121
  /** Host connectivity signal, e.g. "wifi" | "cellular" — read fresh per event via the provider. */
122
122
  networkType?: string;
123
+ /**
124
+ * The rich {@link WireUserContext} to stamp onto every event's `user_context` (device key, opaque
125
+ * user id, opt-in email, arbitrary `extra`). Passed ONCE here at init; updatable post-mount via
126
+ * {@link Analytics.setUserContext} (e.g. attach `userId`/`userEmail` at login). Optional — omit it
127
+ * and events carry only the device context, exactly as before.
128
+ */
129
+ userContext?: WireUserContext;
123
130
  };
124
131
  /** Optional queue tuning knobs, forwarded verbatim to {@link createEventQueue}. */
125
132
  type AnalyticsOptions = Partial<Pick<EventQueueOptions, "maxSize" | "batchSize" | "baseBackoffMs" | "maxBackoffMs" | "maxRetries">>;
@@ -135,6 +142,12 @@ type Analytics = {
135
142
  screen(name: string, props?: AnalyticsProps): void;
136
143
  /** Bind the host's opaque user id (per-session, in-memory) and emit an `identify` event. */
137
144
  identify(userId: string, traits?: AnalyticsProps): void;
145
+ /**
146
+ * Update the {@link WireUserContext} after init (e.g. attach `userId`/`userEmail` at login). Shallow
147
+ * merges the partial over the current context (`extra` is deep-merged); a supplied `userId` also
148
+ * binds like {@link identify}. Takes effect on subsequent events. Fire-and-forget.
149
+ */
150
+ setUserContext(partial: Partial<WireUserContext>): void;
138
151
  /** Attempt an immediate drain of the pending buffer. Fire-and-forget. */
139
152
  flush(): void;
140
153
  /** Host reconnect signal: reset backoff and drain now. Fire-and-forget. */
@@ -3,6 +3,13 @@
3
3
  var react = require('react');
4
4
  var reactNative = require('react-native');
5
5
 
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+
6
13
  // src/reviews/transport.ts
7
14
  var reportAppEvent = (target, name, options = {}) => {
8
15
  if (!(target == null ? void 0 : target.serverUrl) || !name) return;
@@ -64,8 +71,29 @@ var createScreenTracker = (options = {}) => {
64
71
  };
65
72
  var screenTrackingHandler = (tracker) => (state) => tracker.track(getActiveRouteName(state));
66
73
  var useScreenTracking = (navigationRef, options = {}) => {
74
+ const optionsRef = react.useRef(options);
75
+ optionsRef.current = options;
67
76
  const trackerRef = react.useRef(void 0);
68
- if (!trackerRef.current) trackerRef.current = createScreenTracker(options);
77
+ if (!trackerRef.current) {
78
+ const dynamicOptions = {
79
+ get target() {
80
+ return optionsRef.current.target;
81
+ },
82
+ get sessionId() {
83
+ return optionsRef.current.sessionId;
84
+ },
85
+ get deviceKey() {
86
+ return optionsRef.current.deviceKey;
87
+ },
88
+ get onScreen() {
89
+ return optionsRef.current.onScreen;
90
+ },
91
+ get shouldTrack() {
92
+ return optionsRef.current.shouldTrack;
93
+ }
94
+ };
95
+ trackerRef.current = createScreenTracker(dynamicOptions);
96
+ }
69
97
  react.useEffect(() => {
70
98
  const tracker = trackerRef.current;
71
99
  if (!tracker || !(navigationRef == null ? void 0 : navigationRef.addListener)) return;
@@ -136,6 +164,57 @@ var toAnalyticsEvent = (event) => {
136
164
  }
137
165
  }
138
166
  };
167
+
168
+ // src/device/appVersion.ts
169
+ var coerceVersion = (value) => {
170
+ if (typeof value !== "string") return void 0;
171
+ const trimmed = value.trim();
172
+ return trimmed.length > 0 ? trimmed : void 0;
173
+ };
174
+ var runtimeRequire = (moduleName) => {
175
+ try {
176
+ if (typeof __require !== "function") return void 0;
177
+ return __require(moduleName);
178
+ } catch {
179
+ return void 0;
180
+ }
181
+ };
182
+ var interop = (mod) => {
183
+ if (!mod || typeof mod !== "object") return void 0;
184
+ const def = mod.default;
185
+ if (def && typeof def === "object") return def;
186
+ return mod;
187
+ };
188
+ var safeInterop = (requireModule, moduleName) => {
189
+ try {
190
+ return interop(requireModule(moduleName));
191
+ } catch {
192
+ return void 0;
193
+ }
194
+ };
195
+ var detectAppVersion = (requireModule = runtimeRequire) => {
196
+ try {
197
+ const constants = safeInterop(requireModule, "expo-constants");
198
+ if (constants) {
199
+ const expoConfig = constants.expoConfig;
200
+ if (expoConfig && typeof expoConfig === "object") {
201
+ const fromExpoConfig = coerceVersion(expoConfig.version);
202
+ if (fromExpoConfig) return fromExpoConfig;
203
+ }
204
+ const fromNative = coerceVersion(constants.nativeAppVersion);
205
+ if (fromNative) return fromNative;
206
+ }
207
+ const application = safeInterop(requireModule, "expo-application");
208
+ if (application) {
209
+ const fromApplication = coerceVersion(application.nativeApplicationVersion);
210
+ if (fromApplication) return fromApplication;
211
+ }
212
+ } catch {
213
+ }
214
+ return void 0;
215
+ };
216
+
217
+ // src/device/deviceContext.ts
139
218
  var deriveFormFactor = (iosIdiom, width, height) => {
140
219
  if (iosIdiom === "pad") return "tablet";
141
220
  if (iosIdiom === "phone") return "phone";
@@ -205,16 +284,20 @@ var collectDeviceContext = () => {
205
284
  if (resolved.timeZone) ctx.timeZone = resolved.timeZone;
206
285
  } catch {
207
286
  }
287
+ const appVersion = detectAppVersion();
288
+ if (appVersion) ctx.appVersion = appVersion;
208
289
  return ctx;
209
290
  };
210
291
 
211
292
  // src/analytics/contextEnvelope.ts
212
293
  var buildContextEnvelope = (input = {}) => {
294
+ var _a;
213
295
  const device = { ...collectDeviceContext() };
214
- if (input.appVersion && !device.appVersion) device.appVersion = input.appVersion;
296
+ if (input.appVersion) device.appVersion = input.appVersion;
297
+ const effectiveAppVersion = (_a = input.appVersion) != null ? _a : device.appVersion;
215
298
  const envelope = { device };
216
299
  if (input.sessionId) envelope.sessionId = input.sessionId;
217
- if (input.appVersion) envelope.appVersion = input.appVersion;
300
+ if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;
218
301
  if (input.appBuild) envelope.appBuild = input.appBuild;
219
302
  if (input.networkType) envelope.networkType = input.networkType;
220
303
  return envelope;
@@ -338,6 +421,8 @@ var createEventQueue = (options) => {
338
421
  })();
339
422
  const postBatch = async (events) => {
340
423
  if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return false;
424
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
425
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
341
426
  try {
342
427
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
343
428
  const headers = { "Content-Type": "application/json" };
@@ -345,11 +430,14 @@ var createEventQueue = (options) => {
345
430
  const res = await fetch(url, {
346
431
  method: "POST",
347
432
  headers,
348
- body: JSON.stringify({ events })
433
+ body: JSON.stringify({ events }),
434
+ signal: controller == null ? void 0 : controller.signal
349
435
  });
350
436
  return !!(res && res.ok);
351
437
  } catch {
352
438
  return false;
439
+ } finally {
440
+ clearTimeout(timer);
353
441
  }
354
442
  };
355
443
  const clearRetry = () => {
@@ -423,6 +511,16 @@ var createEventQueue = (options) => {
423
511
  return { enqueue, flush, notifyOnline, size };
424
512
  };
425
513
 
514
+ // src/analytics/currentSession.ts
515
+ var _currentSessionId;
516
+ var setCurrentSessionId = (id) => {
517
+ if (typeof id === "string" && id.length > 0) _currentSessionId = id;
518
+ };
519
+ var getCurrentSessionId = () => _currentSessionId;
520
+ var resetCurrentSessionId = () => {
521
+ _currentSessionId = void 0;
522
+ };
523
+
426
524
  // src/identity/userIdentity.ts
427
525
  var USER_ID_MAX_LENGTH = 128;
428
526
  var sanitizeUserId = (raw) => {
@@ -432,12 +530,79 @@ var sanitizeUserId = (raw) => {
432
530
  return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
433
531
  };
434
532
 
533
+ // src/context/userContext.ts
534
+ var EXTRA_KEY_PREFIX = "custom.";
535
+ var isWireScalar = (value) => {
536
+ const t = typeof value;
537
+ if (t === "string" || t === "boolean") return true;
538
+ if (t === "number") return Number.isFinite(value);
539
+ return false;
540
+ };
541
+ var hashEmailFnv1a = (email) => {
542
+ const normalized = email.trim().toLowerCase();
543
+ let hash = 2166136261;
544
+ for (let i = 0; i < normalized.length; i++) {
545
+ hash ^= normalized.charCodeAt(i);
546
+ hash = Math.imul(hash, 16777619);
547
+ }
548
+ return (hash >>> 0).toString(16).padStart(8, "0");
549
+ };
550
+ var cleanString = (value) => {
551
+ if (typeof value !== "string") return void 0;
552
+ const trimmed = value.trim();
553
+ return trimmed.length > 0 ? trimmed : void 0;
554
+ };
555
+ var namespaceExtra = (extra) => {
556
+ const out = {};
557
+ if (!extra || typeof extra !== "object") return out;
558
+ for (const [key, value] of Object.entries(extra)) {
559
+ const cleanKey = cleanString(key);
560
+ if (!cleanKey) continue;
561
+ if (!isWireScalar(value)) continue;
562
+ out[`${EXTRA_KEY_PREFIX}${cleanKey}`] = value;
563
+ }
564
+ return out;
565
+ };
566
+ var resolveUserContext = (ctx = {}, opts = {}) => {
567
+ var _a;
568
+ const result = {};
569
+ const bucket = {};
570
+ const userId = sanitizeUserId(ctx.userId);
571
+ if (userId) result.userId = userId;
572
+ const deviceKey = cleanString(ctx.deviceKey);
573
+ if (deviceKey) {
574
+ result.deviceKey = deviceKey;
575
+ bucket.device_key = deviceKey;
576
+ }
577
+ const appVersion = (_a = cleanString(ctx.appVersion)) != null ? _a : cleanString(opts.autoAppVersion);
578
+ if (appVersion) {
579
+ result.appVersion = appVersion;
580
+ bucket.app_version = appVersion;
581
+ }
582
+ const email = cleanString(ctx.userEmail);
583
+ if (email) {
584
+ if (ctx.hashEmail) {
585
+ bucket.user_email = hashEmailFnv1a(email);
586
+ bucket.user_email_hashed = true;
587
+ } else {
588
+ bucket.user_email = email;
589
+ }
590
+ }
591
+ Object.assign(bucket, namespaceExtra(ctx.extra));
592
+ if (Object.keys(bucket).length > 0) result.userContext = bucket;
593
+ return result;
594
+ };
595
+
435
596
  // src/analytics/analyticsFacade.ts
436
597
  var createAnalytics = (config, options = {}) => {
437
- var _a;
438
- const sessionId = (_a = config.sessionId) != null ? _a : makeSessionId();
598
+ var _a, _b, _c, _d;
599
+ const instanceSessionId = (_a = config.sessionId) != null ? _a : makeSessionId();
600
+ const resolveSessionId = () => {
601
+ var _a2, _b2;
602
+ return (_b2 = (_a2 = config.sessionId) != null ? _a2 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
603
+ };
439
604
  const envelope = () => buildContextEnvelope({
440
- sessionId,
605
+ sessionId: resolveSessionId(),
441
606
  appVersion: config.appVersion,
442
607
  appBuild: config.appBuild,
443
608
  networkType: config.networkType
@@ -449,16 +614,45 @@ var createAnalytics = (config, options = {}) => {
449
614
  envelope,
450
615
  ...options
451
616
  });
452
- let boundUserId;
617
+ let userContext = { ...(_b = config.userContext) != null ? _b : {} };
618
+ let boundUserId = sanitizeUserId((_c = config.userContext) == null ? void 0 : _c.userId);
619
+ const storageKey = `wireai:analytics:userId:${(_d = config.appId) != null ? _d : "default"}`;
620
+ if (config.storage) {
621
+ void config.storage.getItem(storageKey).then((saved) => {
622
+ if (saved && !boundUserId) boundUserId = saved;
623
+ }).catch(() => {
624
+ });
625
+ }
626
+ const applyContext = (event) => {
627
+ var _a2;
628
+ const resolved = resolveUserContext(userContext, { autoAppVersion: config.appVersion });
629
+ if (resolved.userContext) {
630
+ event.user_context = { ...resolved.userContext, ...(_a2 = event.user_context) != null ? _a2 : {} };
631
+ }
632
+ if (boundUserId && !event.user_id) event.user_id = boundUserId;
633
+ };
634
+ const setUserContext = (partial) => {
635
+ var _a2, _b2;
636
+ if (!partial || typeof partial !== "object") return;
637
+ const mergedExtra = partial.extra || userContext.extra ? { ...(_a2 = userContext.extra) != null ? _a2 : {}, ...(_b2 = partial.extra) != null ? _b2 : {} } : void 0;
638
+ userContext = { ...userContext, ...partial };
639
+ if (mergedExtra) userContext.extra = mergedExtra;
640
+ const uid = sanitizeUserId(partial.userId);
641
+ if (uid) {
642
+ boundUserId = uid;
643
+ if (config.storage) void config.storage.setItem(storageKey, uid).catch(() => {
644
+ });
645
+ }
646
+ };
453
647
  const track = (event, props) => {
454
648
  if (!event) return;
455
649
  const clientEvent = {
456
650
  event_type: "app_event",
457
- session_id: sessionId,
651
+ session_id: resolveSessionId(),
458
652
  question_key: event
459
653
  };
460
654
  if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);
461
- if (boundUserId) clientEvent.user_id = boundUserId;
655
+ applyContext(clientEvent);
462
656
  queue.enqueue(clientEvent);
463
657
  };
464
658
  const screen = (name, props) => {
@@ -466,29 +660,37 @@ var createAnalytics = (config, options = {}) => {
466
660
  const meta = { screen: name, ...props != null ? props : {} };
467
661
  const clientEvent = {
468
662
  event_type: "app_event",
469
- session_id: sessionId,
663
+ session_id: resolveSessionId(),
470
664
  question_key: "screen",
471
665
  meta: JSON.stringify(meta)
472
666
  };
473
- if (boundUserId) clientEvent.user_id = boundUserId;
667
+ applyContext(clientEvent);
474
668
  queue.enqueue(clientEvent);
475
669
  };
476
670
  const identify = (userId, traits) => {
477
671
  const clean = sanitizeUserId(userId);
478
672
  if (!clean) return;
479
673
  boundUserId = clean;
674
+ if (config.storage) {
675
+ void config.storage.setItem(storageKey, clean).catch(() => {
676
+ });
677
+ }
480
678
  const clientEvent = {
481
679
  event_type: "identify",
482
- session_id: sessionId,
680
+ // Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to
681
+ // the session it already saw instead of back-filling a phantom `session_started`.
682
+ session_id: resolveSessionId(),
483
683
  user_id: clean
484
684
  };
485
685
  if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
686
+ applyContext(clientEvent);
486
687
  queue.enqueue(clientEvent);
487
688
  };
488
689
  return {
489
690
  track,
490
691
  screen,
491
692
  identify,
693
+ setUserContext,
492
694
  flush: queue.flush,
493
695
  notifyOnline: queue.notifyOnline,
494
696
  size: queue.size
@@ -496,7 +698,12 @@ var createAnalytics = (config, options = {}) => {
496
698
  };
497
699
  var useAnalytics = (config, options = {}) => {
498
700
  const ref = react.useRef(void 0);
499
- if (!ref.current) ref.current = createAnalytics(config, options);
701
+ const prevKeys = react.useRef("");
702
+ const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
703
+ if (!ref.current || prevKeys.current !== currentKeys) {
704
+ prevKeys.current = currentKeys;
705
+ ref.current = createAnalytics(config, options);
706
+ }
500
707
  return ref.current;
501
708
  };
502
709
 
@@ -506,11 +713,14 @@ exports.createAnalytics = createAnalytics;
506
713
  exports.createEventQueue = createEventQueue;
507
714
  exports.createScreenTracker = createScreenTracker;
508
715
  exports.getActiveRouteName = getActiveRouteName;
716
+ exports.getCurrentSessionId = getCurrentSessionId;
509
717
  exports.makeSessionId = makeSessionId;
510
718
  exports.reportAppEvent = reportAppEvent;
511
719
  exports.reportClientEvent = reportClientEvent;
512
720
  exports.reportClientEvents = reportClientEvents;
721
+ exports.resetCurrentSessionId = resetCurrentSessionId;
513
722
  exports.screenTrackingHandler = screenTrackingHandler;
723
+ exports.setCurrentSessionId = setCurrentSessionId;
514
724
  exports.toAnalyticsEvent = toAnalyticsEvent;
515
725
  exports.useAnalytics = useAnalytics;
516
726
  exports.useScreenTracking = useScreenTracking;