@wireai/activation 0.4.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.
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;
@@ -157,6 +164,57 @@ var toAnalyticsEvent = (event) => {
157
164
  }
158
165
  }
159
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
160
218
  var deriveFormFactor = (iosIdiom, width, height) => {
161
219
  if (iosIdiom === "pad") return "tablet";
162
220
  if (iosIdiom === "phone") return "phone";
@@ -226,16 +284,20 @@ var collectDeviceContext = () => {
226
284
  if (resolved.timeZone) ctx.timeZone = resolved.timeZone;
227
285
  } catch {
228
286
  }
287
+ const appVersion = detectAppVersion();
288
+ if (appVersion) ctx.appVersion = appVersion;
229
289
  return ctx;
230
290
  };
231
291
 
232
292
  // src/analytics/contextEnvelope.ts
233
293
  var buildContextEnvelope = (input = {}) => {
294
+ var _a;
234
295
  const device = { ...collectDeviceContext() };
235
- 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;
236
298
  const envelope = { device };
237
299
  if (input.sessionId) envelope.sessionId = input.sessionId;
238
- if (input.appVersion) envelope.appVersion = input.appVersion;
300
+ if (effectiveAppVersion) envelope.appVersion = effectiveAppVersion;
239
301
  if (input.appBuild) envelope.appBuild = input.appBuild;
240
302
  if (input.networkType) envelope.networkType = input.networkType;
241
303
  return envelope;
@@ -449,6 +511,16 @@ var createEventQueue = (options) => {
449
511
  return { enqueue, flush, notifyOnline, size };
450
512
  };
451
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
+
452
524
  // src/identity/userIdentity.ts
453
525
  var USER_ID_MAX_LENGTH = 128;
454
526
  var sanitizeUserId = (raw) => {
@@ -458,12 +530,79 @@ var sanitizeUserId = (raw) => {
458
530
  return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
459
531
  };
460
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
+
461
596
  // src/analytics/analyticsFacade.ts
462
597
  var createAnalytics = (config, options = {}) => {
463
- var _a, _b;
464
- 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
+ };
465
604
  const envelope = () => buildContextEnvelope({
466
- sessionId,
605
+ sessionId: resolveSessionId(),
467
606
  appVersion: config.appVersion,
468
607
  appBuild: config.appBuild,
469
608
  networkType: config.networkType
@@ -475,23 +614,45 @@ var createAnalytics = (config, options = {}) => {
475
614
  envelope,
476
615
  ...options
477
616
  });
478
- let boundUserId;
479
- const storageKey = `wireai:analytics:userId:${(_b = config.appId) != null ? _b : "default"}`;
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"}`;
480
620
  if (config.storage) {
481
621
  void config.storage.getItem(storageKey).then((saved) => {
482
- if (saved) boundUserId = saved;
622
+ if (saved && !boundUserId) boundUserId = saved;
483
623
  }).catch(() => {
484
624
  });
485
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
+ };
486
647
  const track = (event, props) => {
487
648
  if (!event) return;
488
649
  const clientEvent = {
489
650
  event_type: "app_event",
490
- session_id: sessionId,
651
+ session_id: resolveSessionId(),
491
652
  question_key: event
492
653
  };
493
654
  if (props && Object.keys(props).length > 0) clientEvent.meta = JSON.stringify(props);
494
- if (boundUserId) clientEvent.user_id = boundUserId;
655
+ applyContext(clientEvent);
495
656
  queue.enqueue(clientEvent);
496
657
  };
497
658
  const screen = (name, props) => {
@@ -499,11 +660,11 @@ var createAnalytics = (config, options = {}) => {
499
660
  const meta = { screen: name, ...props != null ? props : {} };
500
661
  const clientEvent = {
501
662
  event_type: "app_event",
502
- session_id: sessionId,
663
+ session_id: resolveSessionId(),
503
664
  question_key: "screen",
504
665
  meta: JSON.stringify(meta)
505
666
  };
506
- if (boundUserId) clientEvent.user_id = boundUserId;
667
+ applyContext(clientEvent);
507
668
  queue.enqueue(clientEvent);
508
669
  };
509
670
  const identify = (userId, traits) => {
@@ -516,16 +677,20 @@ var createAnalytics = (config, options = {}) => {
516
677
  }
517
678
  const clientEvent = {
518
679
  event_type: "identify",
519
- 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(),
520
683
  user_id: clean
521
684
  };
522
685
  if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
686
+ applyContext(clientEvent);
523
687
  queue.enqueue(clientEvent);
524
688
  };
525
689
  return {
526
690
  track,
527
691
  screen,
528
692
  identify,
693
+ setUserContext,
529
694
  flush: queue.flush,
530
695
  notifyOnline: queue.notifyOnline,
531
696
  size: queue.size
@@ -548,11 +713,14 @@ exports.createAnalytics = createAnalytics;
548
713
  exports.createEventQueue = createEventQueue;
549
714
  exports.createScreenTracker = createScreenTracker;
550
715
  exports.getActiveRouteName = getActiveRouteName;
716
+ exports.getCurrentSessionId = getCurrentSessionId;
551
717
  exports.makeSessionId = makeSessionId;
552
718
  exports.reportAppEvent = reportAppEvent;
553
719
  exports.reportClientEvent = reportClientEvent;
554
720
  exports.reportClientEvents = reportClientEvents;
721
+ exports.resetCurrentSessionId = resetCurrentSessionId;
555
722
  exports.screenTrackingHandler = screenTrackingHandler;
723
+ exports.setCurrentSessionId = setCurrentSessionId;
556
724
  exports.toAnalyticsEvent = toAnalyticsEvent;
557
725
  exports.useAnalytics = useAnalytics;
558
726
  exports.useScreenTracking = useScreenTracking;