@wireai/activation 0.10.0 → 0.12.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 (61) hide show
  1. package/AGENTS.md +51 -0
  2. package/CHANGELOG.md +111 -1
  3. package/INTEGRATION_PROMPT.md +13 -1
  4. package/README.md +106 -4
  5. package/dist/analytics/index.d.mts +35 -6
  6. package/dist/analytics/index.d.ts +35 -6
  7. package/dist/analytics/index.js +222 -94
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +214 -95
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/{currentSession-D0Vq7_VE.d.ts → currentSession-D6RiVtc8.d.ts} +187 -29
  12. package/dist/{currentSession-DdDkprpM.d.mts → currentSession-DsSDHqor.d.mts} +187 -29
  13. package/dist/index.d.mts +236 -82
  14. package/dist/index.d.ts +236 -82
  15. package/dist/index.js +330 -60
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +314 -61
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/questionnaire/index.d.mts +1 -1
  20. package/dist/questionnaire/index.d.ts +1 -1
  21. package/dist/questionnaire/index.js +59 -8
  22. package/dist/questionnaire/index.js.map +1 -1
  23. package/dist/questionnaire/index.mjs +59 -8
  24. package/dist/questionnaire/index.mjs.map +1 -1
  25. package/dist/reviews/index.d.mts +2 -2
  26. package/dist/reviews/index.d.ts +2 -2
  27. package/dist/reviews/index.js +97 -17
  28. package/dist/reviews/index.js.map +1 -1
  29. package/dist/reviews/index.mjs +97 -17
  30. package/dist/reviews/index.mjs.map +1 -1
  31. package/dist/{transport-Bzb-bcB2.d.mts → transport-CF_eHwzC.d.mts} +15 -16
  32. package/dist/{transport-B31G0Cib.d.ts → transport-DsRe4epC.d.ts} +15 -16
  33. package/llms.txt +9 -0
  34. package/package.json +1 -1
  35. package/src/activation/useWireActivation.ts +12 -1
  36. package/src/activation/wireActivation.ts +36 -24
  37. package/src/analytics/analyticsFacade.ts +113 -29
  38. package/src/analytics/currentSession.ts +83 -0
  39. package/src/analytics/eventQueue.ts +20 -11
  40. package/src/analytics/index.ts +29 -1
  41. package/src/analytics/reportClientEvent.ts +50 -2
  42. package/src/analytics/screenTracking.ts +6 -1
  43. package/src/analytics/useAnalytics.ts +22 -1
  44. package/src/context/deviceId.ts +109 -0
  45. package/src/context/userContext.ts +73 -0
  46. package/src/identity/userIdentity.ts +10 -0
  47. package/src/index.ts +50 -2
  48. package/src/questionnaire/runtime.ts +12 -2
  49. package/src/questionnaire/transport.ts +5 -1
  50. package/src/questionnaire/useQuestionnaireGate.ts +9 -7
  51. package/src/revenuecat/index.ts +55 -0
  52. package/src/revenuecat/purchaseEvents.ts +167 -0
  53. package/src/revenuecat/revenueCatBridge.ts +221 -0
  54. package/src/revenuecat/types.ts +95 -0
  55. package/src/reviews/decision.ts +8 -1
  56. package/src/reviews/runtime.ts +92 -1
  57. package/src/reviews/transport.ts +27 -10
  58. package/src/reviews/useReviewGate.ts +12 -7
  59. package/src/session-analytics/lifecycle.ts +15 -13
  60. package/src/session-analytics/reportSessionStart.ts +15 -4
  61. package/src/session-analytics/useLifecycleEvents.ts +68 -28
@@ -398,27 +398,113 @@ var init_Constants = __esm({
398
398
  }
399
399
  });
400
400
 
401
- // src/reviews/transport.ts
402
- var reportAppEvent = (target, name2, options = {}) => {
403
- if (!(target == null ? void 0 : target.serverUrl) || !name2) return;
401
+ // src/analytics/reportClientEvent.ts
402
+ var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
403
+ var buildEventsRequest = (target, events) => {
404
+ if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
404
405
  try {
405
406
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
406
407
  const headers = { "Content-Type": "application/json" };
407
408
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
409
+ return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
410
+ } catch {
411
+ return null;
412
+ }
413
+ };
414
+ var warnOnSkippedEvents = (res) => {
415
+ try {
416
+ const json = res == null ? void 0 : res.json;
417
+ if (typeof json !== "function") return;
418
+ void Promise.resolve(json.call(res)).then((body) => {
419
+ const skipped = body == null ? void 0 : body.skipped;
420
+ if (typeof skipped !== "number" || skipped <= 0) return;
421
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
422
+ console.warn(
423
+ `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The usual cause is an event with a missing or empty session_id.`
424
+ );
425
+ }
426
+ }).catch(() => {
427
+ });
428
+ } catch {
429
+ }
430
+ };
431
+ var reportClientEvents = (target, events) => {
432
+ try {
433
+ const req = buildEventsRequest(target, events);
434
+ if (!req) return;
435
+ void fetch(req.url, req.init).catch(() => {
436
+ });
437
+ } catch {
438
+ }
439
+ };
440
+ var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
441
+ var reportClientEventsAwait = async (target, events) => {
442
+ try {
443
+ const req = buildEventsRequest(target, events);
444
+ if (!req) return false;
445
+ const res = await fetch(req.url, req.init);
446
+ return Boolean(res && res.ok);
447
+ } catch {
448
+ return false;
449
+ }
450
+ };
451
+ var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
452
+
453
+ // src/analytics/currentSession.ts
454
+ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
455
+ "@wireai/activation:currentSessionId"
456
+ );
457
+ var globalSlot = globalThis;
458
+ var setCurrentSessionId = (id) => {
459
+ if (typeof id === "string" && id.length > 0) {
460
+ globalSlot[CURRENT_SESSION_ID_SLOT] = id;
461
+ }
462
+ };
463
+ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
464
+ var resetCurrentSessionId = () => {
465
+ globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
466
+ };
467
+ var warnInDev = (message) => {
468
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
469
+ console.warn(message);
470
+ return true;
471
+ }
472
+ return false;
473
+ };
474
+ 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.";
475
+ var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
476
+ "@wireai/activation:currentSessionIdMintWarned"
477
+ );
478
+ var warnSlot = globalThis;
479
+ var ensureCurrentSessionId = () => {
480
+ const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
481
+ if (typeof existing === "string" && existing.length > 0) return existing;
482
+ const minted = makeSessionId();
483
+ globalSlot[CURRENT_SESSION_ID_SLOT] = minted;
484
+ if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
485
+ warnSlot[MINT_WARNED_SLOT] = true;
486
+ }
487
+ return minted;
488
+ };
489
+
490
+ // src/reviews/transport.ts
491
+ var reportAppEvent = (target, name2, options = {}) => {
492
+ var _a2;
493
+ if (!(target == null ? void 0 : target.serverUrl) || !name2) return;
494
+ try {
495
+ const supplied = (_a2 = options.sessionId) != null ? _a2 : "";
408
496
  const event = {
409
497
  event_type: "app_event",
410
- question_key: name2
498
+ question_key: name2,
499
+ session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId()
411
500
  };
412
- if (options.sessionId) event.session_id = options.sessionId;
413
501
  if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
414
502
  if (options.meta && Object.keys(options.meta).length > 0) {
415
503
  event.meta = JSON.stringify(options.meta);
416
504
  }
417
- void fetch(url, {
418
- method: "POST",
419
- headers,
420
- body: JSON.stringify({ events: [event] })
421
- }).catch(() => {
505
+ const req = buildEventsRequest(target, [event]);
506
+ if (!req) return;
507
+ void fetch(req.url, req.init).catch(() => {
422
508
  });
423
509
  } catch {
424
510
  }
@@ -495,41 +581,6 @@ var useScreenTracking = (navigationRef, options = {}) => {
495
581
  }, [navigationRef]);
496
582
  };
497
583
 
498
- // src/analytics/reportClientEvent.ts
499
- var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
500
- var buildEventsRequest = (target, events) => {
501
- if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
502
- try {
503
- const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
504
- const headers = { "Content-Type": "application/json" };
505
- if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
506
- return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
507
- } catch {
508
- return null;
509
- }
510
- };
511
- var reportClientEvents = (target, events) => {
512
- try {
513
- const req = buildEventsRequest(target, events);
514
- if (!req) return;
515
- void fetch(req.url, req.init).catch(() => {
516
- });
517
- } catch {
518
- }
519
- };
520
- var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
521
- var reportClientEventsAwait = async (target, events) => {
522
- try {
523
- const req = buildEventsRequest(target, events);
524
- if (!req) return false;
525
- const res = await fetch(req.url, req.init);
526
- return Boolean(res && res.ok);
527
- } catch {
528
- return false;
529
- }
530
- };
531
- var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
532
-
533
584
  // src/analytics/analyticsEvent.ts
534
585
  var WIRE_ONBOARDING_EVENTS = {
535
586
  started: "wire_onboarding_started",
@@ -820,6 +871,7 @@ var createEventQueue = (options) => {
820
871
  var _a3;
821
872
  const env = resolveEnvelope();
822
873
  const stamped = { ...event };
874
+ if (stamped.ts === void 0) stamped.ts = Date.now();
823
875
  if (!env) return stamped;
824
876
  if (!stamped.device && env.device) stamped.device = env.device;
825
877
  if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
@@ -875,19 +927,13 @@ var createEventQueue = (options) => {
875
927
  }
876
928
  })();
877
929
  const postBatch = async (events) => {
878
- if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return false;
930
+ const req = buildEventsRequest(target, events);
931
+ if (!req) return false;
879
932
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
880
933
  const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
881
934
  try {
882
- const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
883
- const headers = { "Content-Type": "application/json" };
884
- if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
885
- const res = await fetch(url, {
886
- method: "POST",
887
- headers,
888
- body: JSON.stringify({ events }),
889
- signal: controller == null ? void 0 : controller.signal
890
- });
935
+ const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
936
+ warnOnSkippedEvents(res);
891
937
  return !!(res && res.ok);
892
938
  } catch {
893
939
  return false;
@@ -966,21 +1012,6 @@ var createEventQueue = (options) => {
966
1012
  return { enqueue, flush, notifyOnline, size };
967
1013
  };
968
1014
 
969
- // src/analytics/currentSession.ts
970
- var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
971
- "@wireai/activation:currentSessionId"
972
- );
973
- var globalSlot = globalThis;
974
- var setCurrentSessionId = (id) => {
975
- if (typeof id === "string" && id.length > 0) {
976
- globalSlot[CURRENT_SESSION_ID_SLOT] = id;
977
- }
978
- };
979
- var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
980
- var resetCurrentSessionId = () => {
981
- globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
982
- };
983
-
984
1015
  // src/identity/userIdentity.ts
985
1016
  var USER_ID_MAX_LENGTH = 128;
986
1017
  var sanitizeUserId = (raw) => {
@@ -989,6 +1020,7 @@ var sanitizeUserId = (raw) => {
989
1020
  if (!trimmed) return void 0;
990
1021
  return trimmed.length > USER_ID_MAX_LENGTH ? trimmed.slice(0, USER_ID_MAX_LENGTH) : trimmed;
991
1022
  };
1023
+ var looksLikeEmail = (value) => typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
992
1024
 
993
1025
  // src/context/userContext.ts
994
1026
  var EXTRA_KEY_PREFIX = "custom.";
@@ -1023,6 +1055,21 @@ var namespaceExtra = (extra) => {
1023
1055
  }
1024
1056
  return out;
1025
1057
  };
1058
+ var analyticsUserIdStorageKey = (appId) => `wireai:analytics:userId:${appId != null ? appId : "default"}`;
1059
+ var clearPiiFromContext = (ctx = {}) => {
1060
+ const rest = {};
1061
+ if (typeof ctx.appVersion === "string") rest.appVersion = ctx.appVersion;
1062
+ if (typeof ctx.deviceKey === "string") rest.deviceKey = ctx.deviceKey;
1063
+ return rest;
1064
+ };
1065
+ var clearUserContext = async (opts = {}) => {
1066
+ const storage = opts.storage;
1067
+ if (!storage) return;
1068
+ try {
1069
+ await storage.removeItem(analyticsUserIdStorageKey(opts.appId));
1070
+ } catch {
1071
+ }
1072
+ };
1026
1073
  var resolveUserContext = (ctx = {}, opts = {}) => {
1027
1074
  var _a2;
1028
1075
  const result = {};
@@ -1061,29 +1108,75 @@ var mintDeviceId = () => {
1061
1108
  const time = Date.now().toString(36);
1062
1109
  return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
1063
1110
  };
1111
+ var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
1112
+ var deviceKeyGlobal = globalThis;
1113
+ var autoDeviceKeyRegistry = () => {
1114
+ const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
1115
+ if (existing) return existing;
1116
+ const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
1117
+ deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
1118
+ return created;
1119
+ };
1120
+ var resolveAutoDeviceKey = (opts = {}) => {
1121
+ var _a2, _b;
1122
+ const registry = autoDeviceKeyRegistry();
1123
+ const appId = (_a2 = opts.appId) != null ? _a2 : "default";
1124
+ let id = registry.keys.get(appId);
1125
+ if (!id) {
1126
+ id = mintDeviceId();
1127
+ registry.keys.set(appId, id);
1128
+ }
1129
+ const storage = opts.storage;
1130
+ if (storage && !registry.hydrating.has(appId)) {
1131
+ registry.hydrating.add(appId);
1132
+ const slot = deviceIdStorageKey(appId);
1133
+ const minted = id;
1134
+ try {
1135
+ void Promise.resolve(storage.getItem(slot)).then((saved) => {
1136
+ const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1137
+ if (persisted) registry.keys.set(appId, persisted);
1138
+ else void Promise.resolve(storage.setItem(slot, minted)).catch(() => {
1139
+ });
1140
+ }).catch(() => {
1141
+ });
1142
+ } catch {
1143
+ }
1144
+ }
1145
+ return (_b = registry.keys.get(appId)) != null ? _b : id;
1146
+ };
1147
+ var resetAutoDeviceKeys = () => {
1148
+ const registry = autoDeviceKeyRegistry();
1149
+ registry.keys.clear();
1150
+ registry.hydrating.clear();
1151
+ };
1064
1152
 
1065
1153
  // src/analytics/analyticsFacade.ts
1154
+ var warnInDev2 = (message) => {
1155
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1156
+ console.warn(message);
1157
+ }
1158
+ };
1066
1159
  var createAnalytics = (config, options = {}) => {
1067
- var _a2, _b, _c, _d, _e;
1160
+ var _a2, _b, _c, _d;
1068
1161
  const instanceSessionId = (_a2 = config.sessionId) != null ? _a2 : makeSessionId();
1069
1162
  const resolveSessionId = () => {
1070
1163
  var _a3, _b2;
1071
1164
  return (_b2 = (_a3 = config.sessionId) != null ? _a3 : getCurrentSessionId()) != null ? _b2 : instanceSessionId;
1072
1165
  };
1166
+ if (config.sessionId) {
1167
+ warnInDev2(
1168
+ "[wireai] createAnalytics({ sessionId }) PINS every event from this instance to that one frozen id and opts out of the live per-open session (app.session_started) \u2014 lifecycle analytics collapse onto a single device-scoped id. Remove it unless your host runs its own session lifecycle."
1169
+ );
1170
+ }
1171
+ const detectedAppVersion = detectAppVersion();
1073
1172
  let userContext = { ...(_b = config.userContext) != null ? _b : {} };
1074
1173
  const hostDeviceKeyAtInit = typeof ((_c = config.userContext) == null ? void 0 : _c.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1075
- let autoDeviceKey = mintDeviceId();
1076
- if (config.storage && !hostDeviceKeyAtInit) {
1077
- const storage = config.storage;
1078
- const deviceKey = deviceIdStorageKey(config.appId);
1079
- void storage.getItem(deviceKey).then((saved) => {
1080
- const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1081
- if (persisted) autoDeviceKey = persisted;
1082
- else void storage.setItem(deviceKey, autoDeviceKey).catch(() => {
1083
- });
1084
- }).catch(() => {
1085
- });
1086
- }
1174
+ const autoDeviceKeyOptions = {
1175
+ appId: config.appId,
1176
+ // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
1177
+ storage: hostDeviceKeyAtInit ? void 0 : config.storage
1178
+ };
1179
+ if (!hostDeviceKeyAtInit) resolveAutoDeviceKey(autoDeviceKeyOptions);
1087
1180
  const envelope = () => {
1088
1181
  var _a3;
1089
1182
  return buildContextEnvelope({
@@ -1101,7 +1194,7 @@ var createAnalytics = (config, options = {}) => {
1101
1194
  ...options
1102
1195
  });
1103
1196
  let boundUserId = sanitizeUserId((_d = config.userContext) == null ? void 0 : _d.userId);
1104
- const storageKey = `wireai:analytics:userId:${(_e = config.appId) != null ? _e : "default"}`;
1197
+ const storageKey = analyticsUserIdStorageKey(config.appId);
1105
1198
  if (config.storage) {
1106
1199
  void config.storage.getItem(storageKey).then((saved) => {
1107
1200
  if (saved && !boundUserId) boundUserId = saved;
@@ -1109,17 +1202,25 @@ var createAnalytics = (config, options = {}) => {
1109
1202
  });
1110
1203
  }
1111
1204
  const applyContext = (event) => {
1112
- var _a3;
1205
+ var _a3, _b2;
1113
1206
  const hostDeviceKey = typeof userContext.deviceKey === "string" && userContext.deviceKey.trim() ? userContext.deviceKey : void 0;
1114
1207
  const resolved = resolveUserContext(
1115
- { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : autoDeviceKey },
1116
- { autoAppVersion: config.appVersion }
1208
+ // `??` is lazy on purpose: a host-supplied key must never even touch the auto registry.
1209
+ { ...userContext, deviceKey: hostDeviceKey != null ? hostDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions) },
1210
+ { autoAppVersion: (_a3 = config.appVersion) != null ? _a3 : detectedAppVersion }
1117
1211
  );
1118
1212
  if (resolved.userContext) {
1119
- event.user_context = { ...resolved.userContext, ...(_a3 = event.user_context) != null ? _a3 : {} };
1213
+ event.user_context = { ...resolved.userContext, ...(_b2 = event.user_context) != null ? _b2 : {} };
1120
1214
  }
1121
1215
  if (boundUserId && !event.user_id) event.user_id = boundUserId;
1122
1216
  };
1217
+ const guardUserId = (clean) => {
1218
+ if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1219
+ warnInDev2(
1220
+ "[wireai] identify() was called with an email-shaped id. A raw email must NOT be the opaque user_id (PII leak) \u2014 pass it as userContext.userEmail instead. Binding was skipped. Set allowEmailAsUserId:true on createAnalytics if your user id genuinely is an email."
1221
+ );
1222
+ return void 0;
1223
+ };
1123
1224
  const setUserContext = (partial) => {
1124
1225
  var _a3, _b2;
1125
1226
  if (!partial || typeof partial !== "object") return;
@@ -1128,11 +1229,20 @@ var createAnalytics = (config, options = {}) => {
1128
1229
  if (mergedExtra) userContext.extra = mergedExtra;
1129
1230
  const uid = sanitizeUserId(partial.userId);
1130
1231
  if (uid) {
1131
- boundUserId = uid;
1132
- if (config.storage) void config.storage.setItem(storageKey, uid).catch(() => {
1133
- });
1232
+ const bindable = guardUserId(uid);
1233
+ if (bindable) {
1234
+ boundUserId = bindable;
1235
+ if (config.storage) void config.storage.setItem(storageKey, bindable).catch(() => {
1236
+ });
1237
+ }
1134
1238
  }
1135
1239
  };
1240
+ const reset = () => {
1241
+ boundUserId = void 0;
1242
+ userContext = clearPiiFromContext(userContext);
1243
+ if (config.storage) void config.storage.removeItem(storageKey).catch(() => {
1244
+ });
1245
+ };
1136
1246
  const track = (event, props) => {
1137
1247
  if (!event) return;
1138
1248
  const clientEvent = {
@@ -1159,9 +1269,11 @@ var createAnalytics = (config, options = {}) => {
1159
1269
  const identify = (userId, traits) => {
1160
1270
  const clean = sanitizeUserId(userId);
1161
1271
  if (!clean) return;
1162
- boundUserId = clean;
1272
+ const bindable = guardUserId(clean);
1273
+ if (!bindable) return;
1274
+ boundUserId = bindable;
1163
1275
  if (config.storage) {
1164
- void config.storage.setItem(storageKey, clean).catch(() => {
1276
+ void config.storage.setItem(storageKey, bindable).catch(() => {
1165
1277
  });
1166
1278
  }
1167
1279
  const clientEvent = {
@@ -1169,7 +1281,7 @@ var createAnalytics = (config, options = {}) => {
1169
1281
  // Reuse the LIVE per-open session id (see `resolveSessionId`) so the server binds identity to
1170
1282
  // the session it already saw instead of back-filling a phantom `session_started`.
1171
1283
  session_id: resolveSessionId(),
1172
- user_id: clean
1284
+ user_id: bindable
1173
1285
  };
1174
1286
  if (traits && Object.keys(traits).length > 0) clientEvent.meta = JSON.stringify(traits);
1175
1287
  applyContext(clientEvent);
@@ -1180,12 +1292,14 @@ var createAnalytics = (config, options = {}) => {
1180
1292
  screen,
1181
1293
  identify,
1182
1294
  setUserContext,
1295
+ reset,
1183
1296
  flush: queue.flush,
1184
1297
  notifyOnline: queue.notifyOnline,
1185
1298
  size: queue.size
1186
1299
  };
1187
1300
  };
1188
1301
  var useAnalytics = (config, options = {}) => {
1302
+ var _a2;
1189
1303
  const ref = useRef(void 0);
1190
1304
  const prevKeys = useRef("");
1191
1305
  const currentKeys = `${config.serverUrl}|${config.apiKey}|${config.appId}`;
@@ -1193,9 +1307,14 @@ var useAnalytics = (config, options = {}) => {
1193
1307
  prevKeys.current = currentKeys;
1194
1308
  ref.current = createAnalytics(config, options);
1195
1309
  }
1310
+ const hostDeviceKey = typeof ((_a2 = config.userContext) == null ? void 0 : _a2.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1311
+ useEffect(() => {
1312
+ var _a3;
1313
+ if (hostDeviceKey) (_a3 = ref.current) == null ? void 0 : _a3.setUserContext({ deviceKey: hostDeviceKey });
1314
+ }, [hostDeviceKey]);
1196
1315
  return ref.current;
1197
1316
  };
1198
1317
 
1199
- export { WIRE_ONBOARDING_EVENTS, buildContextEnvelope, createAnalytics, createEventQueue, createScreenTracker, getActiveRouteName, getCurrentSessionId, makeSessionId, reportAppEvent, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, resetCurrentSessionId, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1318
+ export { AUTO_DEVICE_ID_PREFIX, WIRE_ONBOARDING_EVENTS, analyticsUserIdStorageKey, buildContextEnvelope, clearPiiFromContext, clearUserContext, createAnalytics, createEventQueue, createScreenTracker, deviceIdStorageKey, ensureCurrentSessionId, getActiveRouteName, getCurrentSessionId, looksLikeEmail, makeSessionId, reportAppEvent, reportClientEvent, reportClientEventAwait, reportClientEvents, reportClientEventsAwait, resetAutoDeviceKeys, resetCurrentSessionId, resolveAutoDeviceKey, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1200
1319
  //# sourceMappingURL=index.mjs.map
1201
1320
  //# sourceMappingURL=index.mjs.map