@wireai/activation 0.12.2 → 0.13.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 (43) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +5 -3
  3. package/dist/analytics/index.d.mts +2 -2
  4. package/dist/analytics/index.d.ts +2 -2
  5. package/dist/analytics/index.js +114 -35
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs +114 -36
  8. package/dist/analytics/index.mjs.map +1 -1
  9. package/dist/{currentSession-BxEB37xt.d.ts → currentSession-D7zabMXK.d.ts} +161 -9
  10. package/dist/{currentSession-BlCeDP0f.d.mts → currentSession-_GynvhzT.d.mts} +161 -9
  11. package/dist/index.d.mts +5 -15
  12. package/dist/index.d.ts +5 -15
  13. package/dist/index.js +232 -135
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +229 -134
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/questionnaire/index.js.map +1 -1
  18. package/dist/questionnaire/index.mjs.map +1 -1
  19. package/dist/reviews/index.js +8 -6
  20. package/dist/reviews/index.js.map +1 -1
  21. package/dist/reviews/index.mjs +8 -6
  22. package/dist/reviews/index.mjs.map +1 -1
  23. package/package.json +1 -1
  24. package/src/OnboardingFlow.tsx +10 -3
  25. package/src/WireOnboarding.tsx +115 -32
  26. package/src/activation/wireActivation.ts +13 -7
  27. package/src/analytics/analyticsFacade.ts +11 -10
  28. package/src/analytics/currentSession.ts +6 -20
  29. package/src/analytics/eventQueue.ts +69 -1
  30. package/src/analytics/index.ts +1 -1
  31. package/src/analytics/reportClientEvent.ts +92 -29
  32. package/src/config/wireConfigFromEnv.ts +1 -10
  33. package/src/context/deviceId.ts +77 -16
  34. package/src/context/userContext.ts +4 -15
  35. package/src/identity/identityRecord.ts +123 -0
  36. package/src/identity/userIdentity.ts +45 -9
  37. package/src/index.ts +6 -4
  38. package/src/session-analytics/useLifecycleEvents.ts +10 -1
  39. package/src/types.ts +14 -0
  40. package/src/utils/deriveAnswers.ts +6 -2
  41. package/src/utils/readProgress.ts +4 -0
  42. package/src/utils/warnInDev.ts +33 -0
  43. package/src/components/DoneBlock.tsx +0 -37
@@ -411,28 +411,35 @@ var buildEventsRequest = (target, events) => {
411
411
  return null;
412
412
  }
413
413
  };
414
- var warnOnSkippedEvents = (res) => {
415
- if (typeof __DEV__ === "undefined" || !__DEV__) return;
416
- if (typeof console === "undefined" || !console.warn) return;
414
+ var readEventsAck = async (res) => {
417
415
  try {
418
416
  const json = res == null ? void 0 : res.json;
419
- if (typeof json !== "function") return;
420
- void Promise.resolve(json.call(res)).then((body) => {
421
- const skipped = body == null ? void 0 : body.skipped;
422
- if (typeof skipped !== "number" || skipped <= 0) return;
423
- console.warn(
424
- `[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.`
425
- );
426
- }).catch(() => {
427
- });
417
+ if (typeof json !== "function") return void 0;
418
+ const body = await Promise.resolve(json.call(res));
419
+ const skipped = body == null ? void 0 : body.skipped;
420
+ if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
421
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => e == null ? void 0 : e.reason).filter((r) => typeof r === "string") : [];
422
+ return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
428
423
  } catch {
424
+ return void 0;
429
425
  }
430
426
  };
427
+ var describeDiscarded = (ack) => `[wireai] the server ACCEPTED the /v1/events POST but DISCARDED ${ack.skipped} event(s) (skipped in the response body) \u2014 they are gone, not retried. The server reported: skipped=${ack.skipped}${ack.written !== void 0 ? `, written=${ack.written}` : ""}` + (ack.reasons.length > 0 ? `, reasons: ${ack.reasons.join(", ")}.` : ". It gave no reason (the endpoint folds every rejection into one count), so check the server's ingest log for this request rather than guessing.");
428
+ var warnOnSkippedEvents = (res) => {
429
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
430
+ if (typeof console === "undefined" || !console.warn) return;
431
+ void readEventsAck(res).then((ack) => {
432
+ if (!ack || ack.skipped <= 0) return;
433
+ console.warn(describeDiscarded(ack));
434
+ });
435
+ };
431
436
  var reportClientEvents = (target, events) => {
432
437
  try {
433
438
  const req = buildEventsRequest(target, events);
434
439
  if (!req) return;
435
- void fetch(req.url, req.init).catch(() => {
440
+ void fetch(req.url, req.init).then((res) => {
441
+ warnOnSkippedEvents(res);
442
+ }).catch(() => {
436
443
  });
437
444
  } catch {
438
445
  }
@@ -443,13 +450,28 @@ var reportClientEventsAwait = async (target, events) => {
443
450
  const req = buildEventsRequest(target, events);
444
451
  if (!req) return false;
445
452
  const res = await fetch(req.url, req.init);
446
- return Boolean(res && res.ok);
453
+ if (!res || !res.ok) return false;
454
+ const ack = await readEventsAck(res);
455
+ if (!ack || ack.skipped <= 0) return true;
456
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
457
+ console.warn(describeDiscarded(ack));
458
+ }
459
+ return false;
447
460
  } catch {
448
461
  return false;
449
462
  }
450
463
  };
451
464
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
452
465
 
466
+ // src/utils/warnInDev.ts
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
+
453
475
  // src/analytics/currentSession.ts
454
476
  var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
455
477
  "@wireai/activation:currentSessionId"
@@ -464,13 +486,6 @@ var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
464
486
  var resetCurrentSessionId = () => {
465
487
  globalSlot[CURRENT_SESSION_ID_SLOT] = void 0;
466
488
  };
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
489
  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
490
  var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
476
491
  "@wireai/activation:currentSessionIdMintWarned"
@@ -818,6 +833,31 @@ var DEFAULTS = {
818
833
  maxRetries: 6
819
834
  };
820
835
  var READ_TIMEOUT_MS = 1500;
836
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
837
+ var queueKeyGlobal = globalThis;
838
+ var claimedQueueKeys = () => {
839
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
840
+ if (existing) return existing;
841
+ const created = /* @__PURE__ */ new Set();
842
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
843
+ return created;
844
+ };
845
+ var claimQueueKey = (preferred, explicit) => {
846
+ const claimed = claimedQueueKeys();
847
+ if (explicit || !claimed.has(preferred)) {
848
+ claimed.add(preferred);
849
+ return preferred;
850
+ }
851
+ let ordinal = 2;
852
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
853
+ const key = `${preferred}#${ordinal}`;
854
+ claimed.add(key);
855
+ warnInDev(
856
+ `[wireai] a second event queue was created for the same appId, and "${preferred}" is already claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, delete each other's pending events when one drains to empty, and double-send on relaunch, so this queue was given "${key}" instead. Prefer ONE analytics instance per app; if you really need two, pass an explicit \`storageKey\` to each so the slots are yours to reason about.`
857
+ );
858
+ return key;
859
+ };
860
+ var resetEventQueueKeys = () => claimedQueueKeys().clear();
821
861
  var withTimeout = (p, ms) => {
822
862
  let timer;
823
863
  const timeout = new Promise((resolve) => {
@@ -849,7 +889,8 @@ var createEventQueue = (options) => {
849
889
  var _a2, _b, _c, _d, _e, _f, _g;
850
890
  const target = options.target;
851
891
  const storage = options.storage;
852
- const key = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
892
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a2 = options.appId) != null ? _a2 : "default"}`;
893
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
853
894
  const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
854
895
  const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
855
896
  const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
@@ -1100,6 +1141,29 @@ var resolveUserContext = (ctx = {}, opts = {}) => {
1100
1141
  return result;
1101
1142
  };
1102
1143
 
1144
+ // src/identity/identityRecord.ts
1145
+ var IDENTITY_PROVENANCE_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:identityProvenance");
1146
+ var provenanceGlobal = globalThis;
1147
+ var provenanceRegistry = () => {
1148
+ const existing = provenanceGlobal[IDENTITY_PROVENANCE_SLOT];
1149
+ if (existing) return existing;
1150
+ const created = { host: /* @__PURE__ */ new Map() };
1151
+ provenanceGlobal[IDENTITY_PROVENANCE_SLOT] = created;
1152
+ return created;
1153
+ };
1154
+ var provenanceKey = (space, scope) => `${space}:${scope != null ? scope : "default"}`;
1155
+ var resolveIdentity = (input) => {
1156
+ var _a2;
1157
+ if (typeof input.value !== "string") return void 0;
1158
+ const value = input.value.trim();
1159
+ if (!value) return void 0;
1160
+ const durable = (_a2 = input.durable) != null ? _a2 : input.source === "host";
1161
+ {
1162
+ provenanceRegistry().host.set(provenanceKey(input.space, input.scope), value);
1163
+ }
1164
+ return { value, space: input.space, source: input.source, durable };
1165
+ };
1166
+
1103
1167
  // src/context/deviceId.ts
1104
1168
  var AUTO_DEVICE_ID_PREFIX = "wdev_";
1105
1169
  var deviceIdStorageKey = (appId) => `wireai:analytics:deviceKey:${appId != null ? appId : "default"}`;
@@ -1122,24 +1186,37 @@ var startHydration = (registry, appId, storage, minted) => {
1122
1186
  const existing = registry.pending.get(appId);
1123
1187
  if (existing) return existing;
1124
1188
  const slot = deviceIdStorageKey(appId);
1125
- const settled = () => {
1189
+ const degraded = () => {
1126
1190
  var _a2;
1127
- return (_a2 = registry.keys.get(appId)) != null ? _a2 : minted;
1191
+ return { value: (_a2 = registry.keys.get(appId)) != null ? _a2 : minted, durable: false };
1128
1192
  };
1193
+ const adopted = (value) => ({ value, durable: true });
1129
1194
  let run;
1130
1195
  try {
1131
1196
  run = Promise.resolve(storage.getItem(slot)).then((saved) => {
1132
1197
  const persisted = typeof saved === "string" && saved.trim() ? saved.trim() : void 0;
1133
1198
  if (persisted) {
1134
1199
  registry.keys.set(appId, persisted);
1135
- return persisted;
1200
+ return adopted(persisted);
1136
1201
  }
1137
- return Promise.resolve(storage.setItem(slot, minted)).then(settled, settled);
1138
- }).catch(settled);
1202
+ return Promise.resolve(storage.setItem(slot, minted)).then(
1203
+ () => {
1204
+ var _a2;
1205
+ return adopted((_a2 = registry.keys.get(appId)) != null ? _a2 : minted);
1206
+ },
1207
+ degraded
1208
+ );
1209
+ }).catch(degraded);
1139
1210
  } catch {
1140
- run = Promise.resolve(settled());
1211
+ run = Promise.resolve(degraded());
1141
1212
  }
1142
1213
  registry.pending.set(appId, run);
1214
+ void run.then((outcome) => {
1215
+ var _a2;
1216
+ if (outcome.durable) return;
1217
+ (_a2 = registry.pending) == null ? void 0 : _a2.delete(appId);
1218
+ registry.hydrating.delete(appId);
1219
+ });
1143
1220
  return run;
1144
1221
  };
1145
1222
  var resolveAutoDeviceKey = (opts = {}) => {
@@ -1167,11 +1244,6 @@ var resetAutoDeviceKeys = () => {
1167
1244
  };
1168
1245
 
1169
1246
  // src/analytics/analyticsFacade.ts
1170
- var warnInDev2 = (message) => {
1171
- if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1172
- console.warn(message);
1173
- }
1174
- };
1175
1247
  var createAnalytics = (config, options = {}) => {
1176
1248
  var _a2, _b, _c;
1177
1249
  const resolveSessionId = () => {
@@ -1179,13 +1251,19 @@ var createAnalytics = (config, options = {}) => {
1179
1251
  return (_a3 = config.sessionId) != null ? _a3 : ensureCurrentSessionId();
1180
1252
  };
1181
1253
  if (config.sessionId) {
1182
- warnInDev2(
1254
+ warnInDev(
1183
1255
  "[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."
1184
1256
  );
1185
1257
  }
1186
1258
  const detectedAppVersion = detectAppVersion();
1187
1259
  let userContext = { ...(_a2 = config.userContext) != null ? _a2 : {} };
1188
1260
  const hostDeviceKeyAtInit = typeof ((_b = config.userContext) == null ? void 0 : _b.deviceKey) === "string" && config.userContext.deviceKey.trim() ? config.userContext.deviceKey.trim() : void 0;
1261
+ resolveIdentity({
1262
+ value: hostDeviceKeyAtInit,
1263
+ space: "device",
1264
+ source: "host",
1265
+ scope: config.appId
1266
+ });
1189
1267
  const autoDeviceKeyOptions = {
1190
1268
  appId: config.appId,
1191
1269
  // A host-supplied deviceKey opts out of minting AND persisting (unchanged contract).
@@ -1231,7 +1309,7 @@ var createAnalytics = (config, options = {}) => {
1231
1309
  };
1232
1310
  const guardUserId = (clean) => {
1233
1311
  if (config.allowEmailAsUserId || !looksLikeEmail(clean)) return clean;
1234
- warnInDev2(
1312
+ warnInDev(
1235
1313
  "[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."
1236
1314
  );
1237
1315
  return void 0;
@@ -1330,6 +1408,6 @@ var useAnalytics = (config, options = {}) => {
1330
1408
  return ref.current;
1331
1409
  };
1332
1410
 
1333
- 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 };
1411
+ 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, resetEventQueueKeys, resolveAutoDeviceKey, screenTrackingHandler, setCurrentSessionId, toAnalyticsEvent, useAnalytics, useScreenTracking };
1334
1412
  //# sourceMappingURL=index.mjs.map
1335
1413
  //# sourceMappingURL=index.mjs.map