@wireai/activation 0.13.6-next.0 → 0.14.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 (42) hide show
  1. package/AGENTS.md +21 -9
  2. package/CHANGELOG.md +258 -3
  3. package/INTEGRATION_PROMPT.md +7 -4
  4. package/README.md +41 -35
  5. package/dist/analytics/index.d.mts +9 -2
  6. package/dist/analytics/index.d.ts +9 -2
  7. package/dist/analytics/index.js +56 -12
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +56 -12
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/{currentSession-Cs3lweFZ.d.mts → currentSession-CUvTOchb.d.mts} +62 -7
  12. package/dist/{currentSession-DD6dKB0i.d.ts → currentSession-CW_5Mq4O.d.ts} +62 -7
  13. package/dist/index.d.mts +26 -6
  14. package/dist/index.d.ts +26 -6
  15. package/dist/index.js +635 -544
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +635 -544
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/questionnaire/index.js.map +1 -1
  20. package/dist/questionnaire/index.mjs.map +1 -1
  21. package/dist/reviews/index.js.map +1 -1
  22. package/dist/reviews/index.mjs.map +1 -1
  23. package/llms.txt +3 -3
  24. package/package.json +1 -1
  25. package/src/OnboardingFlow.tsx +23 -5
  26. package/src/WireOnboarding.tsx +4 -3
  27. package/src/activation/useWireActivation.ts +14 -1
  28. package/src/activation/wireActivation.ts +68 -2
  29. package/src/analytics/analyticsFacade.ts +24 -0
  30. package/src/analytics/eventQueue.ts +106 -11
  31. package/src/analytics/reportClientEvent.ts +31 -7
  32. package/src/analytics/useAnalytics.ts +17 -0
  33. package/src/context/deviceId.ts +10 -3
  34. package/src/permissions/permissionMemory.ts +12 -1
  35. package/src/session/persistedSession.ts +32 -8
  36. package/src/session-analytics/lifecycle.ts +26 -5
  37. package/src/session-analytics/reportSessionStart.ts +14 -10
  38. package/src/session-analytics/useLifecycleEvents.ts +70 -32
  39. package/src/session-analytics/useSessionStart.ts +57 -15
  40. package/src/types.ts +43 -8
  41. package/src/utils/readPlan.ts +26 -10
  42. package/src/utils/readProgress.ts +5 -0
package/dist/index.mjs CHANGED
@@ -1155,7 +1155,10 @@ var readProgress = (response) => {
1155
1155
  key: typeof p.key === "string" ? p.key : void 0,
1156
1156
  // Whitelisted the same way as every other field: an old backend simply omits it.
1157
1157
  slot_id: typeof p.slot_id === "string" ? p.slot_id : void 0,
1158
- skippable: typeof p.skippable === "boolean" ? p.skippable : void 0
1158
+ skippable: typeof p.skippable === "boolean" ? p.skippable : void 0,
1159
+ // Whitelisted like the rest: a non-string (or absent) arm key reads as "no experiment", never
1160
+ // as an error. The kit does not interpret the value — see `OnboardingProgress.variant`.
1161
+ variant: typeof p.variant === "string" ? p.variant : void 0
1159
1162
  };
1160
1163
  };
1161
1164
 
@@ -1225,20 +1228,21 @@ var reportClientEvents = (target, events) => {
1225
1228
  }
1226
1229
  };
1227
1230
  var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
1228
- var reportClientEventsAwait = async (target, events) => {
1231
+ var reportClientEventsAwait = async (target, events) => await reportClientEventsOutcome(target, events) === "delivered";
1232
+ var reportClientEventsOutcome = async (target, events) => {
1229
1233
  try {
1230
1234
  const req = buildEventsRequest(target, events);
1231
- if (!req) return false;
1235
+ if (!req) return "refused";
1232
1236
  const res = await fetch(req.url, req.init);
1233
- if (!res || !res.ok) return false;
1237
+ if (!res || !res.ok) return "unreachable";
1234
1238
  const ack = await readEventsAck(res);
1235
- if (!ack || ack.skipped <= 0) return true;
1239
+ if (!ack || ack.skipped <= 0) return "delivered";
1236
1240
  if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1237
1241
  console.warn(describeDiscarded(ack));
1238
1242
  }
1239
- return false;
1243
+ return "refused";
1240
1244
  } catch {
1241
- return false;
1245
+ return "unreachable";
1242
1246
  }
1243
1247
  };
1244
1248
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
@@ -1697,6 +1701,7 @@ var OnboardingFlow = ({
1697
1701
  const clientContextRef = useRef(clientContext);
1698
1702
  clientContextRef.current = clientContext;
1699
1703
  const lastScreenIndexRef = useRef(-1);
1704
+ const variantRef = useRef(void 0);
1700
1705
  const previewTimer = useRef(null);
1701
1706
  const [runId, setRunId] = useState(0);
1702
1707
  const startedRunRef = useRef(-1);
@@ -1746,14 +1751,17 @@ var OnboardingFlow = ({
1746
1751
  useEffect(() => {
1747
1752
  var _a2, _b2, _c2, _d2;
1748
1753
  setValidationError(void 0);
1754
+ if (variantRef.current === void 0 && progress.variant) variantRef.current = progress.variant;
1749
1755
  if (!(lastCard == null ? void 0 : lastCard.id)) return;
1750
1756
  const step2 = (_a2 = progress.step) != null ? _a2 : renderedCount;
1751
1757
  lastScreenIndexRef.current = Math.max(0, step2 - 1);
1752
- (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, {
1758
+ const turn = {
1753
1759
  type: "turn",
1754
1760
  step: step2,
1755
1761
  component: (_b2 = lastCard.response) == null ? void 0 : _b2.component
1756
- });
1762
+ };
1763
+ if (variantRef.current !== void 0) turn.variant = variantRef.current;
1764
+ (_c2 = onEventRef.current) == null ? void 0 : _c2.call(onEventRef, turn);
1757
1765
  if (((_d2 = lastCard.response) == null ? void 0 : _d2.component) === "InterstitialCard") {
1758
1766
  sendPreview(reportTargetRef.current, {
1759
1767
  sessionId: sessionIdRef.current,
@@ -1814,6 +1822,7 @@ var OnboardingFlow = ({
1814
1822
  const result = { answers: deriveAnswers(messages), raw: messages };
1815
1823
  const plan = readPlan(messages);
1816
1824
  if (plan !== void 0) result.plan = plan;
1825
+ if (variantRef.current !== void 0) result.variant = variantRef.current;
1817
1826
  onCompleteRef.current(result);
1818
1827
  }, [messages]);
1819
1828
  const handleRetry = useCallback(() => {
@@ -3608,10 +3617,11 @@ var ensureCurrentSessionId = () => {
3608
3617
  var DEFAULT_SESSION_TTL_MS = 36e5;
3609
3618
  var READ_TIMEOUT_MS = 1500;
3610
3619
  var sessionStorageKey = (appId) => `wireai:session:${appId}`;
3620
+ var READ_TIMED_OUT = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
3611
3621
  var withTimeout = (p, ms) => {
3612
3622
  let timer;
3613
3623
  const timeout = new Promise((resolve) => {
3614
- timer = setTimeout(() => resolve(void 0), ms);
3624
+ timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);
3615
3625
  });
3616
3626
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
3617
3627
  };
@@ -3627,12 +3637,14 @@ var parsePersisted = (raw) => {
3627
3637
  return void 0;
3628
3638
  };
3629
3639
  var loadPersistedSession = async (storage, key, ttlMs = DEFAULT_SESSION_TTL_MS) => {
3630
- let stored;
3640
+ let raw;
3631
3641
  try {
3632
- stored = parsePersisted(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS));
3642
+ raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
3633
3643
  } catch {
3634
- stored = void 0;
3644
+ raw = null;
3635
3645
  }
3646
+ if (raw === READ_TIMED_OUT) return { id: makeSessionId(), resumed: false };
3647
+ const stored = parsePersisted(raw);
3636
3648
  if (stored && Date.now() - stored.ts < ttlMs) {
3637
3649
  return { id: stored.id, resumed: true };
3638
3650
  }
@@ -3642,7 +3654,9 @@ var loadPersistedSession = async (storage, key, ttlMs = DEFAULT_SESSION_TTL_MS)
3642
3654
  };
3643
3655
  var peekPersistedSession = async (storage, key) => {
3644
3656
  try {
3645
- return parsePersisted(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS));
3657
+ const raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
3658
+ if (raw === READ_TIMED_OUT) return void 0;
3659
+ return parsePersisted(raw);
3646
3660
  } catch {
3647
3661
  return void 0;
3648
3662
  }
@@ -3795,7 +3809,9 @@ var readSettledPermissions = (raw, sessionId) => {
3795
3809
  };
3796
3810
  var loadSettledPermissions = async (storage, key, sessionId) => {
3797
3811
  try {
3798
- return readSettledPermissions(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS), sessionId);
3812
+ const raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
3813
+ if (raw === READ_TIMED_OUT) return [];
3814
+ return readSettledPermissions(raw, sessionId);
3799
3815
  } catch {
3800
3816
  return [];
3801
3817
  }
@@ -3904,7 +3920,7 @@ var WireOnboarding = ({
3904
3920
  useEffect(() => {
3905
3921
  if (!warnMissingJoinKey) return;
3906
3922
  warnInDev(
3907
- "[wireai] <WireOnboarding> got no user_context.device_key, so this onboarding session can never be joined to the app's later events and the `activated` funnel will read zero. Pass userContext={activationJoinContext(deviceKey)} \u2014 and if your app owns no device id, userContext={activationJoinContext(resolveAutoDeviceKey({ appId, storage }))} returns the SAME id the analytics side stamps. Never hand-write userContext={{ deviceKey }}. The kit did NOT auto-inject its own key here because " + autoJoinReason
3923
+ "[wireai] <WireOnboarding> got no user_context.device_key, so this onboarding session can never be joined to the app's later events and the `activated` funnel will read zero. Pass userContext={activationJoinContext(deviceKey)} if your app owns a device id. If it does NOT, do not build one here \u2014 leave userContext alone and pass a working `storage` instead: the kit injects the same id the analytics side stamps, and only when it has confirmed the id actually persists. Never hand-write userContext={{ deviceKey }}. The kit did NOT auto-inject its own key here because " + autoJoinReason
3908
3924
  );
3909
3925
  }, [warnMissingJoinKey, autoJoinReason]);
3910
3926
  const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
@@ -4583,198 +4599,511 @@ var attributionMetadata = (a) => {
4583
4599
  return { attribution };
4584
4600
  };
4585
4601
 
4586
- // src/activation/revalidation.ts
4587
- var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4588
- "@wireai/activation:activationRevalidation"
4589
- );
4590
- var globalSlot2 = globalThis;
4591
- var store = () => {
4592
- const existing = globalSlot2[REVALIDATION_SLOT];
4602
+ // src/analytics/eventQueue.ts
4603
+ var DEFAULTS = {
4604
+ maxSize: 200,
4605
+ batchSize: 20,
4606
+ baseBackoffMs: 1e3,
4607
+ maxBackoffMs: 3e4,
4608
+ maxRetries: 6
4609
+ };
4610
+ var READ_TIMEOUT_MS3 = 1500;
4611
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
4612
+ var queueKeyGlobal = globalThis;
4613
+ var claimedQueueKeys = () => {
4614
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
4593
4615
  if (existing) return existing;
4594
- const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4595
- globalSlot2[REVALIDATION_SLOT] = created;
4616
+ const created = /* @__PURE__ */ new Set();
4617
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
4596
4618
  return created;
4597
4619
  };
4598
- var bumpActivationRevalidation = () => {
4599
- const s = store();
4600
- s.version += 1;
4601
- for (const listener of Array.from(s.listeners)) {
4602
- try {
4603
- listener();
4604
- } catch {
4605
- }
4620
+ var claimQueueKey = (preferred, explicit) => {
4621
+ const claimed = claimedQueueKeys();
4622
+ if (explicit || !claimed.has(preferred)) {
4623
+ claimed.add(preferred);
4624
+ return preferred;
4606
4625
  }
4626
+ let ordinal = 2;
4627
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
4628
+ const key = `${preferred}#${ordinal}`;
4629
+ claimed.add(key);
4630
+ warnInDev(
4631
+ `[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.`
4632
+ );
4633
+ return key;
4607
4634
  };
4608
- var subscribeActivationRevalidation = (listener) => {
4609
- const s = store();
4610
- s.listeners.add(listener);
4611
- return () => {
4612
- s.listeners.delete(listener);
4613
- };
4614
- };
4615
- var getActivationRevalidationVersion = () => store().version;
4616
- var resetActivationRevalidation = () => {
4617
- const s = store();
4618
- s.version = 0;
4619
- s.listeners.clear();
4635
+ var releaseQueueKey = (key) => {
4636
+ claimedQueueKeys().delete(key);
4620
4637
  };
4621
-
4622
- // src/activation/wireActivation.ts
4623
- var clean = cleanString;
4624
- var createWireActivation = (config) => {
4625
- var _a, _b;
4626
- const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4627
- const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4628
- resolveIdentity({
4629
- value: explicitDeviceKey,
4630
- space: "device",
4631
- source: "host",
4632
- scope: config.appId
4638
+ var READ_TIMED_OUT2 = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
4639
+ var withTimeout3 = (p, ms) => {
4640
+ let timer;
4641
+ const timeout = new Promise((resolve) => {
4642
+ timer = setTimeout(() => resolve(READ_TIMED_OUT2), ms);
4633
4643
  });
4634
- const autoDeviceKeyOptions = {
4635
- appId: config.appId,
4636
- // An explicit key opts out of minting AND persisting (unchanged contract).
4637
- storage: explicitDeviceKey ? void 0 : config.storage
4638
- };
4639
- if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4640
- const detectedAppVersion = detectAppVersion();
4641
- const applyContext = (event) => {
4642
- var _a2, _b2, _c;
4643
- const resolved = resolveUserContext(
4644
- // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4645
- {
4646
- ...(_a2 = config.userContext) != null ? _a2 : {},
4647
- deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4648
- },
4649
- { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4650
- );
4651
- if (resolved.userContext) {
4652
- event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4653
- }
4654
- if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4655
- };
4656
- const track = async (name, meta) => {
4657
- if (!clean(name)) return false;
4658
- const sessionId = ensureCurrentSessionId();
4659
- const event = {
4660
- event_type: "app_event",
4661
- session_id: sessionId,
4662
- question_key: name
4663
- };
4664
- if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
4665
- applyContext(event);
4666
- const ok = await reportClientEventAwait(target, event);
4667
- if (ok) bumpActivationRevalidation();
4668
- return ok;
4669
- };
4670
- return {
4671
- track,
4672
- get sessionId() {
4673
- return getCurrentSessionId();
4674
- },
4675
- subscribeRevalidation: subscribeActivationRevalidation,
4676
- getRevalidationVersion: getActivationRevalidationVersion
4677
- };
4678
- };
4679
- var useActivationRevalidation = () => useSyncExternalStore(
4680
- subscribeActivationRevalidation,
4681
- getActivationRevalidationVersion,
4682
- getActivationRevalidationVersion
4683
- );
4684
- var useWireActivation = (config) => {
4685
- var _a;
4686
- const ref = useRef(void 0);
4687
- const prevKeys = useRef("");
4688
- const currentKeys = [
4689
- config.serverUrl,
4690
- config.apiKey,
4691
- config.appId,
4692
- config.deviceKey,
4693
- (_a = config.userContext) == null ? void 0 : _a.deviceKey
4694
- ].join("|");
4695
- if (!ref.current || prevKeys.current !== currentKeys) {
4696
- prevKeys.current = currentKeys;
4697
- ref.current = createWireActivation(config);
4698
- }
4699
- const revalidation = useActivationRevalidation();
4700
- return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
4701
- };
4702
-
4703
- // src/revenuecat/purchaseEvents.ts
4704
- var WIRE_PURCHASE_EVENTS = {
4705
- /** The paywall became visible (offerings loaded). */
4706
- paywallShown: "wire_paywall_shown",
4707
- /** The user tapped buy; the store sheet is about to open. */
4708
- checkoutStarted: "wire_checkout_started",
4709
- /** The store confirmed the purchase AND the entitlement is now active. */
4710
- purchased: "wire_purchase_completed",
4711
- /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
4712
- purchaseFailed: "wire_purchase_failed",
4713
- /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
4714
- restored: "wire_purchase_restored"
4715
- };
4716
- var PLAN_TIER_CONTEXT_KEY = "plan_tier";
4717
- var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
4718
- var activeEntitlement = (info, entitlementId) => {
4719
- var _a, _b;
4720
- const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
4721
- const found = active == null ? void 0 : active[entitlementId];
4722
- const entitlement = asRecord(found);
4723
- if (!entitlement) return void 0;
4724
- return entitlement.isActive === false ? void 0 : entitlement;
4644
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4725
4645
  };
4726
- var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
4727
- var resolvePlanTier = (info, entitlementId) => {
4728
- const entitlement = activeEntitlement(info, entitlementId);
4729
- if (!entitlement) return "free";
4730
- return isTrial(entitlement) ? "trial" : "paid";
4646
+ var unrefTimer = (timer) => {
4647
+ const t = timer;
4648
+ if (typeof t.unref === "function") t.unref();
4731
4649
  };
4732
- var describePackage = (pkg) => {
4733
- const props = {};
4734
- if (!pkg) return props;
4735
- if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
4736
- if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
4737
- if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
4738
- const product = asRecord(pkg.product);
4739
- if (product) {
4740
- if (typeof product.identifier === "string") props.product_id = product.identifier;
4741
- if (typeof product.price === "number" && Number.isFinite(product.price)) {
4742
- props.price = product.price;
4650
+ var parsePersisted2 = (raw) => {
4651
+ if (!raw) return [];
4652
+ try {
4653
+ const parsed = JSON.parse(raw);
4654
+ if (!Array.isArray(parsed)) return [];
4655
+ const items = [];
4656
+ for (const entry of parsed) {
4657
+ if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
4658
+ items.push(entry);
4659
+ }
4743
4660
  }
4744
- if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
4661
+ return items;
4662
+ } catch {
4663
+ return [];
4745
4664
  }
4746
- return props;
4747
4665
  };
4748
- var describeEntitlement = (info, entitlementId) => {
4749
- const entitlement = activeEntitlement(info, entitlementId);
4750
- const props = {
4751
- entitlement: entitlementId,
4752
- plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
4666
+ var createEventQueue = (options) => {
4667
+ var _a, _b, _c, _d, _e, _f, _g;
4668
+ const target = options.target;
4669
+ const storage = options.storage;
4670
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
4671
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4672
+ const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4673
+ const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4674
+ const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
4675
+ const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
4676
+ const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
4677
+ let pending = [];
4678
+ let nextId = 0;
4679
+ let flushing = false;
4680
+ let attempt = 0;
4681
+ let retryTimer;
4682
+ let disposed = false;
4683
+ let backlogUnread = storage !== void 0;
4684
+ const resolveEnvelope = () => {
4685
+ try {
4686
+ return typeof options.envelope === "function" ? options.envelope() : options.envelope;
4687
+ } catch {
4688
+ return void 0;
4689
+ }
4753
4690
  };
4754
- if (!entitlement) return props;
4755
- if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
4756
- props.is_trial = isTrial(entitlement);
4757
- if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
4758
- if (typeof entitlement.store === "string") props.store = entitlement.store;
4759
- if (typeof entitlement.productIdentifier === "string") {
4760
- props.product_id = entitlement.productIdentifier;
4761
- }
4762
- if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
4763
- return props;
4764
- };
4765
- var PURCHASE_CANCELLED_CODE = "1";
4766
- var isUserCancelled = (error) => {
4767
- const record = asRecord(error);
4768
- if (!record) return false;
4769
- if (record.userCancelled === true) return true;
4770
- return record.code === PURCHASE_CANCELLED_CODE;
4771
- };
4772
- var describeFailure = (error) => {
4773
- const record = asRecord(error);
4774
- const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
4775
- return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
4776
- };
4777
-
4691
+ const stamp = (event) => {
4692
+ var _a2;
4693
+ const env = resolveEnvelope();
4694
+ const stamped = { ...event };
4695
+ if (stamped.ts === void 0) stamped.ts = Date.now();
4696
+ if (!env) return stamped;
4697
+ if (!stamped.device && env.device) stamped.device = env.device;
4698
+ if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
4699
+ const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
4700
+ if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
4701
+ if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
4702
+ if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
4703
+ if (Object.keys(uc).length > 0) stamped.user_context = uc;
4704
+ return stamped;
4705
+ };
4706
+ const persist = () => {
4707
+ if (!storage) return;
4708
+ if (disposed) return;
4709
+ if (backlogUnread) return;
4710
+ try {
4711
+ if (pending.length === 0) {
4712
+ void storage.removeItem(key).catch(() => {
4713
+ });
4714
+ return;
4715
+ }
4716
+ const payload = pending.map((item) => ({ id: item.id, event: item.event }));
4717
+ void storage.setItem(key, JSON.stringify(payload)).catch(() => {
4718
+ });
4719
+ } catch {
4720
+ }
4721
+ };
4722
+ const isCountedOpenEvent = (event) => typeof event.question_key === "string" && event.question_key.startsWith("app.");
4723
+ const enforceSizeCap = () => {
4724
+ let overflow = pending.length - maxSize;
4725
+ if (overflow <= 0) return;
4726
+ const kept = [];
4727
+ for (const item of pending) {
4728
+ if (overflow > 0 && !isCountedOpenEvent(item.event)) {
4729
+ overflow--;
4730
+ continue;
4731
+ }
4732
+ kept.push(item);
4733
+ }
4734
+ if (overflow > 0) kept.splice(0, overflow);
4735
+ pending = kept;
4736
+ };
4737
+ const safeSig = (event) => {
4738
+ try {
4739
+ return JSON.stringify(event);
4740
+ } catch {
4741
+ return `__nosig_${nextId}_${Math.random()}`;
4742
+ }
4743
+ };
4744
+ const mergePersisted = (persistedItems) => {
4745
+ if (persistedItems.length === 0) return;
4746
+ const seen = new Set(pending.map((item) => item.sig));
4747
+ const restored = [];
4748
+ for (const persisted of persistedItems) {
4749
+ const sig = safeSig(persisted.event);
4750
+ if (seen.has(sig)) continue;
4751
+ seen.add(sig);
4752
+ restored.push({ id: nextId++, event: persisted.event, sig });
4753
+ }
4754
+ if (restored.length === 0) return;
4755
+ pending = [...restored, ...pending];
4756
+ enforceSizeCap();
4757
+ persist();
4758
+ };
4759
+ const loadPromise = (async () => {
4760
+ if (!storage) return;
4761
+ try {
4762
+ const read = Promise.resolve(storage.getItem(key));
4763
+ read.catch(() => {
4764
+ });
4765
+ const raced = await withTimeout3(read, READ_TIMEOUT_MS3);
4766
+ if (raced === READ_TIMED_OUT2) {
4767
+ backlogUnread = true;
4768
+ warnInDev(
4769
+ `[wireai] the persisted analytics backlog at "${key}" took longer than ${READ_TIMEOUT_MS3}ms to read, so the queue started without it. The stored events are NOT discarded: writes are held back until the read lands, and the backlog is merged in then. If you see this on every cold start, your storage adapter is too slow to be on the launch path.`
4770
+ );
4771
+ void read.then((late) => {
4772
+ backlogUnread = false;
4773
+ mergePersisted(parsePersisted2(late));
4774
+ flush();
4775
+ }).catch(() => {
4776
+ backlogUnread = false;
4777
+ persist();
4778
+ });
4779
+ return;
4780
+ }
4781
+ backlogUnread = false;
4782
+ mergePersisted(parsePersisted2(raced));
4783
+ persist();
4784
+ } catch {
4785
+ backlogUnread = false;
4786
+ persist();
4787
+ }
4788
+ })();
4789
+ const postBatch = async (events) => {
4790
+ const req = buildEventsRequest(target, events);
4791
+ if (!req) return false;
4792
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
4793
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
4794
+ try {
4795
+ const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
4796
+ warnOnSkippedEvents(res);
4797
+ return !!(res && res.ok);
4798
+ } catch {
4799
+ return false;
4800
+ } finally {
4801
+ clearTimeout(timer);
4802
+ }
4803
+ };
4804
+ const clearRetry = () => {
4805
+ if (retryTimer !== void 0) {
4806
+ clearTimeout(retryTimer);
4807
+ retryTimer = void 0;
4808
+ }
4809
+ };
4810
+ const scheduleRetry = () => {
4811
+ if (attempt >= maxRetries) return;
4812
+ const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
4813
+ attempt++;
4814
+ clearRetry();
4815
+ retryTimer = setTimeout(() => {
4816
+ retryTimer = void 0;
4817
+ void drain();
4818
+ }, delay);
4819
+ unrefTimer(retryTimer);
4820
+ };
4821
+ const drain = async () => {
4822
+ if (disposed) return;
4823
+ try {
4824
+ await loadPromise;
4825
+ } catch {
4826
+ }
4827
+ if (disposed) return;
4828
+ if (flushing) return;
4829
+ flushing = true;
4830
+ try {
4831
+ while (pending.length > 0) {
4832
+ const batch = pending.slice(0, batchSize);
4833
+ const ok = await postBatch(batch.map((item) => item.event));
4834
+ if (!ok) {
4835
+ scheduleRetry();
4836
+ return;
4837
+ }
4838
+ const acked = new Set(batch.map((item) => item.id));
4839
+ pending = pending.filter((item) => !acked.has(item.id));
4840
+ persist();
4841
+ attempt = 0;
4842
+ clearRetry();
4843
+ }
4844
+ } finally {
4845
+ flushing = false;
4846
+ }
4847
+ };
4848
+ const flush = () => {
4849
+ try {
4850
+ void drain();
4851
+ } catch {
4852
+ }
4853
+ };
4854
+ const enqueue = (event) => {
4855
+ if (disposed) return;
4856
+ try {
4857
+ const stamped = stamp(event);
4858
+ const sig = safeSig(stamped);
4859
+ for (const item of pending) {
4860
+ if (item.sig === sig) return;
4861
+ }
4862
+ pending.push({ id: nextId++, event: stamped, sig });
4863
+ enforceSizeCap();
4864
+ persist();
4865
+ if (retryTimer === void 0) flush();
4866
+ } catch {
4867
+ }
4868
+ };
4869
+ const notifyOnline = () => {
4870
+ attempt = 0;
4871
+ clearRetry();
4872
+ flush();
4873
+ };
4874
+ const size = () => pending.length;
4875
+ const dispose = () => {
4876
+ if (disposed) return;
4877
+ disposed = true;
4878
+ clearRetry();
4879
+ if (storage) releaseQueueKey(key);
4880
+ };
4881
+ return { enqueue, flush, notifyOnline, size, dispose };
4882
+ };
4883
+
4884
+ // src/activation/revalidation.ts
4885
+ var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4886
+ "@wireai/activation:activationRevalidation"
4887
+ );
4888
+ var globalSlot2 = globalThis;
4889
+ var store = () => {
4890
+ const existing = globalSlot2[REVALIDATION_SLOT];
4891
+ if (existing) return existing;
4892
+ const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4893
+ globalSlot2[REVALIDATION_SLOT] = created;
4894
+ return created;
4895
+ };
4896
+ var bumpActivationRevalidation = () => {
4897
+ const s = store();
4898
+ s.version += 1;
4899
+ for (const listener of Array.from(s.listeners)) {
4900
+ try {
4901
+ listener();
4902
+ } catch {
4903
+ }
4904
+ }
4905
+ };
4906
+ var subscribeActivationRevalidation = (listener) => {
4907
+ const s = store();
4908
+ s.listeners.add(listener);
4909
+ return () => {
4910
+ s.listeners.delete(listener);
4911
+ };
4912
+ };
4913
+ var getActivationRevalidationVersion = () => store().version;
4914
+ var resetActivationRevalidation = () => {
4915
+ const s = store();
4916
+ s.version = 0;
4917
+ s.listeners.clear();
4918
+ };
4919
+
4920
+ // src/activation/wireActivation.ts
4921
+ var clean = cleanString;
4922
+ var createWireActivation = (config) => {
4923
+ var _a, _b;
4924
+ const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4925
+ const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4926
+ resolveIdentity({
4927
+ value: explicitDeviceKey,
4928
+ space: "device",
4929
+ source: "host",
4930
+ scope: config.appId
4931
+ });
4932
+ const autoDeviceKeyOptions = {
4933
+ appId: config.appId,
4934
+ // An explicit key opts out of minting AND persisting (unchanged contract).
4935
+ storage: explicitDeviceKey ? void 0 : config.storage
4936
+ };
4937
+ if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4938
+ const detectedAppVersion = detectAppVersion();
4939
+ const applyContext = (event) => {
4940
+ var _a2, _b2, _c;
4941
+ const resolved = resolveUserContext(
4942
+ // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4943
+ {
4944
+ ...(_a2 = config.userContext) != null ? _a2 : {},
4945
+ deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4946
+ },
4947
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4948
+ );
4949
+ if (resolved.userContext) {
4950
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4951
+ }
4952
+ if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4953
+ };
4954
+ let durability;
4955
+ const bufferFailedEvent = (event) => {
4956
+ var _a2;
4957
+ if (config.sink) {
4958
+ config.sink(event);
4959
+ return;
4960
+ }
4961
+ if (!durability) {
4962
+ durability = createEventQueue({
4963
+ target,
4964
+ storage: config.storage,
4965
+ storageKey: `wireai:evtq:activation:${(_a2 = config.appId) != null ? _a2 : "default"}`
4966
+ });
4967
+ }
4968
+ durability.enqueue(event);
4969
+ };
4970
+ const track = async (name, meta) => {
4971
+ if (!clean(name)) return false;
4972
+ const sessionId = ensureCurrentSessionId();
4973
+ const event = {
4974
+ event_type: "app_event",
4975
+ session_id: sessionId,
4976
+ question_key: name
4977
+ };
4978
+ if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
4979
+ applyContext(event);
4980
+ const outcome = await reportClientEventsOutcome(target, [event]);
4981
+ const ok = outcome === "delivered";
4982
+ if (ok) bumpActivationRevalidation();
4983
+ if (outcome === "unreachable") bufferFailedEvent(event);
4984
+ return ok;
4985
+ };
4986
+ return {
4987
+ track,
4988
+ get sessionId() {
4989
+ return getCurrentSessionId();
4990
+ },
4991
+ subscribeRevalidation: subscribeActivationRevalidation,
4992
+ getRevalidationVersion: getActivationRevalidationVersion,
4993
+ dispose: () => {
4994
+ durability == null ? void 0 : durability.dispose();
4995
+ durability = void 0;
4996
+ }
4997
+ };
4998
+ };
4999
+ var useActivationRevalidation = () => useSyncExternalStore(
5000
+ subscribeActivationRevalidation,
5001
+ getActivationRevalidationVersion,
5002
+ getActivationRevalidationVersion
5003
+ );
5004
+ var useWireActivation = (config) => {
5005
+ var _a, _b;
5006
+ const ref = useRef(void 0);
5007
+ const prevKeys = useRef("");
5008
+ const currentKeys = [
5009
+ config.serverUrl,
5010
+ config.apiKey,
5011
+ config.appId,
5012
+ config.deviceKey,
5013
+ (_a = config.userContext) == null ? void 0 : _a.deviceKey
5014
+ ].join("|");
5015
+ if (!ref.current || prevKeys.current !== currentKeys) {
5016
+ prevKeys.current = currentKeys;
5017
+ (_b = ref.current) == null ? void 0 : _b.dispose();
5018
+ ref.current = createWireActivation(config);
5019
+ }
5020
+ useEffect(
5021
+ () => () => {
5022
+ var _a2;
5023
+ (_a2 = ref.current) == null ? void 0 : _a2.dispose();
5024
+ ref.current = void 0;
5025
+ },
5026
+ []
5027
+ );
5028
+ const revalidation = useActivationRevalidation();
5029
+ return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
5030
+ };
5031
+
5032
+ // src/revenuecat/purchaseEvents.ts
5033
+ var WIRE_PURCHASE_EVENTS = {
5034
+ /** The paywall became visible (offerings loaded). */
5035
+ paywallShown: "wire_paywall_shown",
5036
+ /** The user tapped buy; the store sheet is about to open. */
5037
+ checkoutStarted: "wire_checkout_started",
5038
+ /** The store confirmed the purchase AND the entitlement is now active. */
5039
+ purchased: "wire_purchase_completed",
5040
+ /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
5041
+ purchaseFailed: "wire_purchase_failed",
5042
+ /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
5043
+ restored: "wire_purchase_restored"
5044
+ };
5045
+ var PLAN_TIER_CONTEXT_KEY = "plan_tier";
5046
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
5047
+ var activeEntitlement = (info, entitlementId) => {
5048
+ var _a, _b;
5049
+ const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
5050
+ const found = active == null ? void 0 : active[entitlementId];
5051
+ const entitlement = asRecord(found);
5052
+ if (!entitlement) return void 0;
5053
+ return entitlement.isActive === false ? void 0 : entitlement;
5054
+ };
5055
+ var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
5056
+ var resolvePlanTier = (info, entitlementId) => {
5057
+ const entitlement = activeEntitlement(info, entitlementId);
5058
+ if (!entitlement) return "free";
5059
+ return isTrial(entitlement) ? "trial" : "paid";
5060
+ };
5061
+ var describePackage = (pkg) => {
5062
+ const props = {};
5063
+ if (!pkg) return props;
5064
+ if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
5065
+ if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
5066
+ if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
5067
+ const product = asRecord(pkg.product);
5068
+ if (product) {
5069
+ if (typeof product.identifier === "string") props.product_id = product.identifier;
5070
+ if (typeof product.price === "number" && Number.isFinite(product.price)) {
5071
+ props.price = product.price;
5072
+ }
5073
+ if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
5074
+ }
5075
+ return props;
5076
+ };
5077
+ var describeEntitlement = (info, entitlementId) => {
5078
+ const entitlement = activeEntitlement(info, entitlementId);
5079
+ const props = {
5080
+ entitlement: entitlementId,
5081
+ plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
5082
+ };
5083
+ if (!entitlement) return props;
5084
+ if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
5085
+ props.is_trial = isTrial(entitlement);
5086
+ if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
5087
+ if (typeof entitlement.store === "string") props.store = entitlement.store;
5088
+ if (typeof entitlement.productIdentifier === "string") {
5089
+ props.product_id = entitlement.productIdentifier;
5090
+ }
5091
+ if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
5092
+ return props;
5093
+ };
5094
+ var PURCHASE_CANCELLED_CODE = "1";
5095
+ var isUserCancelled = (error) => {
5096
+ const record = asRecord(error);
5097
+ if (!record) return false;
5098
+ if (record.userCancelled === true) return true;
5099
+ return record.code === PURCHASE_CANCELLED_CODE;
5100
+ };
5101
+ var describeFailure = (error) => {
5102
+ const record = asRecord(error);
5103
+ const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
5104
+ return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
5105
+ };
5106
+
4778
5107
  // src/revenuecat/revenueCatBridge.ts
4779
5108
  var createRevenueCatBridge = (config) => {
4780
5109
  const { analytics, entitlementId } = config;
@@ -4892,15 +5221,9 @@ var reportSessionStart = (opts) => {
4892
5221
  opts.sink(event);
4893
5222
  return;
4894
5223
  }
4895
- if (!(target == null ? void 0 : target.serverUrl)) return;
4896
- const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
4897
- const headers = { "Content-Type": "application/json" };
4898
- if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
4899
- void fetch(url, {
4900
- method: "POST",
4901
- headers,
4902
- body: JSON.stringify({ events: [event] })
4903
- }).then((res) => {
5224
+ const req = buildEventsRequest(target, [event]);
5225
+ if (!req) return;
5226
+ void fetch(req.url, req.init).then((res) => {
4904
5227
  warnOnSkippedEvents(res);
4905
5228
  }).catch(() => {
4906
5229
  });
@@ -4912,13 +5235,25 @@ var useSessionStart = (config, options = {}) => {
4912
5235
  const latest = useRef({ config, options });
4913
5236
  latest.current = { config, options };
4914
5237
  useEffect(() => {
4915
- const resolveDeviceKey = (cfg, opts) => {
5238
+ let cancelled = false;
5239
+ const resolveDeviceKey = (opts, autoDeviceKey) => {
4916
5240
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4917
5241
  if (host) return host;
4918
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4919
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5242
+ return autoDeviceKey;
5243
+ };
5244
+ const openAutoDeviceKey = (fireOpen) => {
5245
+ const { config: cfg, options: opts } = latest.current;
5246
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5247
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5248
+ fireOpen(void 0);
5249
+ return;
5250
+ }
5251
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5252
+ if (cancelled) return;
5253
+ fireOpen((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5254
+ });
4920
5255
  };
4921
- const fire = () => {
5256
+ const fire = (autoDeviceKey) => {
4922
5257
  var _a, _b;
4923
5258
  const { config: cfg, options: opts } = latest.current;
4924
5259
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return;
@@ -4930,7 +5265,7 @@ var useSessionStart = (config, options = {}) => {
4930
5265
  target,
4931
5266
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4932
5267
  userId: opts.userId,
4933
- deviceKey: resolveDeviceKey(cfg, opts),
5268
+ deviceKey: resolveDeviceKey(opts, autoDeviceKey),
4934
5269
  sessionCount: opts.sessionCount,
4935
5270
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4936
5271
  platform: Platform.OS,
@@ -4938,7 +5273,7 @@ var useSessionStart = (config, options = {}) => {
4938
5273
  meta: opts.meta
4939
5274
  });
4940
5275
  };
4941
- fire();
5276
+ openAutoDeviceKey(fire);
4942
5277
  let backgroundedAt = null;
4943
5278
  const onChange = (state) => {
4944
5279
  if (state === "background" || state === "inactive") {
@@ -4948,11 +5283,12 @@ var useSessionStart = (config, options = {}) => {
4948
5283
  if (state === "active") {
4949
5284
  const since = backgroundedAt;
4950
5285
  backgroundedAt = null;
4951
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fire();
5286
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) openAutoDeviceKey(fire);
4952
5287
  }
4953
5288
  };
4954
5289
  const sub = AppState.addEventListener("change", onChange);
4955
5290
  return () => {
5291
+ cancelled = true;
4956
5292
  if (sub && typeof sub.remove === "function") sub.remove();
4957
5293
  };
4958
5294
  }, []);
@@ -4961,11 +5297,12 @@ var useSessionStart = (config, options = {}) => {
4961
5297
  // src/session-analytics/lifecycle.ts
4962
5298
  var FIRST_OPEN_EVENT = "app.first_open";
4963
5299
  var firstOpenStorageKey = (appId) => `wireai:first_open:${appId}`;
4964
- var READ_TIMEOUT_MS3 = 1500;
4965
- var withTimeout3 = (p, ms) => {
5300
+ var READ_TIMEOUT_MS4 = 1500;
5301
+ var READ_TIMED_OUT3 = /* @__PURE__ */ Symbol("wireai:first-open-read-timeout");
5302
+ var withTimeout4 = (p, ms) => {
4966
5303
  let timer;
4967
5304
  const timeout = new Promise((resolve) => {
4968
- timer = setTimeout(() => resolve(void 0), ms);
5305
+ timer = setTimeout(() => resolve(READ_TIMED_OUT3), ms);
4969
5306
  });
4970
5307
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4971
5308
  };
@@ -4984,335 +5321,82 @@ var buildLifecycleEvent = (questionKey, opts) => {
4984
5321
  if (opts.appVersion) userContext.app_version = opts.appVersion;
4985
5322
  if (opts.platform) userContext.platform = opts.platform;
4986
5323
  const event = {
4987
- event_type: "app_event",
4988
- question_key: questionKey,
4989
- session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
4990
- };
4991
- const userId = sanitizeUserId(opts.userId);
4992
- if (userId) event.user_id = userId;
4993
- if (Object.keys(userContext).length > 0) event.user_context = userContext;
4994
- if (opts.device) event.device = opts.device;
4995
- if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
4996
- return event;
4997
- };
4998
- var routeLifecycleEvent = (event, opts) => {
4999
- try {
5000
- if (opts.sink) {
5001
- opts.sink(event);
5002
- return;
5003
- }
5004
- const req = buildEventsRequest(opts.target, [event]);
5005
- if (!req) return;
5006
- void fetch(req.url, req.init).catch(() => {
5007
- });
5008
- } catch {
5009
- }
5010
- };
5011
- var emitFirstOpen = (opts) => {
5012
- routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5013
- };
5014
- var reportFirstOpen = (opts) => {
5015
- var _a;
5016
- const appId = (_a = opts.appId) != null ? _a : "default";
5017
- if (_firstOpenLatched.has(appId)) return;
5018
- _firstOpenLatched.add(appId);
5019
- const storage = opts.storage;
5020
- if (!storage) {
5021
- emitFirstOpen(opts);
5022
- return;
5023
- }
5024
- const key = firstOpenStorageKey(appId);
5025
- void (async () => {
5026
- try {
5027
- const seen = await withTimeout3(storage.getItem(key), READ_TIMEOUT_MS3);
5028
- if (seen) return;
5029
- emitFirstOpen(opts);
5030
- try {
5031
- void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5032
- });
5033
- } catch {
5034
- }
5035
- } catch {
5036
- emitFirstOpen(opts);
5037
- }
5038
- })();
5039
- };
5040
- var wireLifecycleEvents = (opts) => {
5041
- var _a;
5042
- const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5043
- reportFirstOpen({ ...opts, sessionId });
5044
- reportSessionStart({
5045
- target: opts.target,
5046
- sink: opts.sink,
5047
- sessionId,
5048
- userId: opts.userId,
5049
- deviceKey: opts.deviceKey,
5050
- sessionCount: opts.sessionCount,
5051
- appVersion: opts.appVersion,
5052
- platform: opts.platform,
5053
- device: opts.device,
5054
- meta: opts.meta
5055
- });
5056
- };
5057
-
5058
- // src/analytics/eventQueue.ts
5059
- var DEFAULTS = {
5060
- maxSize: 200,
5061
- batchSize: 20,
5062
- baseBackoffMs: 1e3,
5063
- maxBackoffMs: 3e4,
5064
- maxRetries: 6
5065
- };
5066
- var READ_TIMEOUT_MS4 = 1500;
5067
- var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
5068
- var queueKeyGlobal = globalThis;
5069
- var claimedQueueKeys = () => {
5070
- const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
5071
- if (existing) return existing;
5072
- const created = /* @__PURE__ */ new Set();
5073
- queueKeyGlobal[QUEUE_KEY_SLOT] = created;
5074
- return created;
5075
- };
5076
- var claimQueueKey = (preferred, explicit) => {
5077
- const claimed = claimedQueueKeys();
5078
- if (explicit || !claimed.has(preferred)) {
5079
- claimed.add(preferred);
5080
- return preferred;
5081
- }
5082
- let ordinal = 2;
5083
- while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
5084
- const key = `${preferred}#${ordinal}`;
5085
- claimed.add(key);
5086
- warnInDev(
5087
- `[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.`
5088
- );
5089
- return key;
5090
- };
5091
- var READ_TIMED_OUT = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
5092
- var withTimeout4 = (p, ms) => {
5093
- let timer;
5094
- const timeout = new Promise((resolve) => {
5095
- timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);
5096
- });
5097
- return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
5098
- };
5099
- var unrefTimer = (timer) => {
5100
- const t = timer;
5101
- if (typeof t.unref === "function") t.unref();
5324
+ event_type: "app_event",
5325
+ question_key: questionKey,
5326
+ session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
5327
+ };
5328
+ const userId = sanitizeUserId(opts.userId);
5329
+ if (userId) event.user_id = userId;
5330
+ if (Object.keys(userContext).length > 0) event.user_context = userContext;
5331
+ if (opts.device) event.device = opts.device;
5332
+ if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
5333
+ return event;
5102
5334
  };
5103
- var parsePersisted2 = (raw) => {
5104
- if (!raw) return [];
5335
+ var routeLifecycleEvent = (event, opts) => {
5105
5336
  try {
5106
- const parsed = JSON.parse(raw);
5107
- if (!Array.isArray(parsed)) return [];
5108
- const items = [];
5109
- for (const entry of parsed) {
5110
- if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
5111
- items.push(entry);
5112
- }
5337
+ if (opts.sink) {
5338
+ opts.sink(event);
5339
+ return;
5113
5340
  }
5114
- return items;
5341
+ const req = buildEventsRequest(opts.target, [event]);
5342
+ if (!req) return;
5343
+ void fetch(req.url, req.init).catch(() => {
5344
+ });
5115
5345
  } catch {
5116
- return [];
5117
5346
  }
5118
5347
  };
5119
- var createEventQueue = (options) => {
5120
- var _a, _b, _c, _d, _e, _f, _g;
5121
- const target = options.target;
5122
- const storage = options.storage;
5123
- const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
5124
- const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
5125
- const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
5126
- const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
5127
- const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
5128
- const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
5129
- const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
5130
- let pending = [];
5131
- let nextId = 0;
5132
- let flushing = false;
5133
- let attempt = 0;
5134
- let retryTimer;
5135
- let backlogUnread = false;
5136
- const resolveEnvelope = () => {
5137
- try {
5138
- return typeof options.envelope === "function" ? options.envelope() : options.envelope;
5139
- } catch {
5140
- return void 0;
5141
- }
5142
- };
5143
- const stamp = (event) => {
5144
- var _a2;
5145
- const env = resolveEnvelope();
5146
- const stamped = { ...event };
5147
- if (stamped.ts === void 0) stamped.ts = Date.now();
5148
- if (!env) return stamped;
5149
- if (!stamped.device && env.device) stamped.device = env.device;
5150
- if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
5151
- const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
5152
- if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
5153
- if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
5154
- if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
5155
- if (Object.keys(uc).length > 0) stamped.user_context = uc;
5156
- return stamped;
5157
- };
5158
- const persist = () => {
5159
- if (!storage) return;
5160
- if (backlogUnread) return;
5161
- try {
5162
- if (pending.length === 0) {
5163
- void storage.removeItem(key).catch(() => {
5164
- });
5165
- return;
5166
- }
5167
- const payload = pending.map((item) => ({ id: item.id, event: item.event }));
5168
- void storage.setItem(key, JSON.stringify(payload)).catch(() => {
5169
- });
5170
- } catch {
5171
- }
5172
- };
5173
- const enforceSizeCap = () => {
5174
- if (pending.length > maxSize) pending.splice(0, pending.length - maxSize);
5175
- };
5176
- const safeSig = (event) => {
5177
- try {
5178
- return JSON.stringify(event);
5179
- } catch {
5180
- return `__nosig_${nextId}_${Math.random()}`;
5181
- }
5182
- };
5183
- const mergePersisted = (persistedItems) => {
5184
- if (persistedItems.length === 0) return;
5185
- const seen = new Set(pending.map((item) => item.sig));
5186
- const restored = [];
5187
- for (const persisted of persistedItems) {
5188
- const sig = safeSig(persisted.event);
5189
- if (seen.has(sig)) continue;
5190
- seen.add(sig);
5191
- restored.push({ id: nextId++, event: persisted.event, sig });
5192
- }
5193
- if (restored.length === 0) return;
5194
- pending = [...restored, ...pending];
5195
- enforceSizeCap();
5196
- persist();
5197
- };
5198
- const loadPromise = (async () => {
5199
- if (!storage) return;
5348
+ var emitFirstOpen = (opts) => {
5349
+ routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5350
+ };
5351
+ var reportFirstOpen = (opts) => {
5352
+ var _a;
5353
+ const appId = (_a = opts.appId) != null ? _a : "default";
5354
+ if (_firstOpenLatched.has(appId)) return;
5355
+ _firstOpenLatched.add(appId);
5356
+ const storage = opts.storage;
5357
+ if (!storage) {
5358
+ emitFirstOpen(opts);
5359
+ return;
5360
+ }
5361
+ const key = firstOpenStorageKey(appId);
5362
+ void (async () => {
5200
5363
  try {
5201
- const read = Promise.resolve(storage.getItem(key));
5202
- read.catch(() => {
5203
- });
5204
- const raced = await withTimeout4(read, READ_TIMEOUT_MS4);
5205
- if (raced === READ_TIMED_OUT) {
5206
- backlogUnread = true;
5207
- warnInDev(
5208
- `[wireai] the persisted analytics backlog at "${key}" took longer than ${READ_TIMEOUT_MS4}ms to read, so the queue started without it. The stored events are NOT discarded: writes are held back until the read lands, and the backlog is merged in then. If you see this on every cold start, your storage adapter is too slow to be on the launch path.`
5209
- );
5210
- void read.then((late) => {
5211
- backlogUnread = false;
5212
- mergePersisted(parsePersisted2(late));
5213
- flush();
5214
- }).catch(() => {
5215
- backlogUnread = false;
5216
- persist();
5364
+ const seen = await withTimeout4(storage.getItem(key), READ_TIMEOUT_MS4);
5365
+ if (seen === READ_TIMED_OUT3) return;
5366
+ if (seen) return;
5367
+ emitFirstOpen(opts);
5368
+ try {
5369
+ void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5217
5370
  });
5218
- return;
5371
+ } catch {
5219
5372
  }
5220
- mergePersisted(parsePersisted2(raced));
5221
5373
  } catch {
5222
5374
  }
5223
5375
  })();
5224
- const postBatch = async (events) => {
5225
- const req = buildEventsRequest(target, events);
5226
- if (!req) return false;
5227
- const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
5228
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
5229
- try {
5230
- const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
5231
- warnOnSkippedEvents(res);
5232
- return !!(res && res.ok);
5233
- } catch {
5234
- return false;
5235
- } finally {
5236
- clearTimeout(timer);
5237
- }
5238
- };
5239
- const clearRetry = () => {
5240
- if (retryTimer !== void 0) {
5241
- clearTimeout(retryTimer);
5242
- retryTimer = void 0;
5243
- }
5244
- };
5245
- const scheduleRetry = () => {
5246
- if (attempt >= maxRetries) return;
5247
- const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
5248
- attempt++;
5249
- clearRetry();
5250
- retryTimer = setTimeout(() => {
5251
- retryTimer = void 0;
5252
- void drain();
5253
- }, delay);
5254
- unrefTimer(retryTimer);
5255
- };
5256
- const drain = async () => {
5257
- try {
5258
- await loadPromise;
5259
- } catch {
5260
- }
5261
- if (flushing) return;
5262
- flushing = true;
5263
- try {
5264
- while (pending.length > 0) {
5265
- const batch = pending.slice(0, batchSize);
5266
- const ok = await postBatch(batch.map((item) => item.event));
5267
- if (!ok) {
5268
- scheduleRetry();
5269
- return;
5270
- }
5271
- const acked = new Set(batch.map((item) => item.id));
5272
- pending = pending.filter((item) => !acked.has(item.id));
5273
- persist();
5274
- attempt = 0;
5275
- clearRetry();
5276
- }
5277
- } finally {
5278
- flushing = false;
5279
- }
5280
- };
5281
- const flush = () => {
5282
- try {
5283
- void drain();
5284
- } catch {
5285
- }
5286
- };
5287
- const enqueue = (event) => {
5288
- try {
5289
- const stamped = stamp(event);
5290
- const sig = safeSig(stamped);
5291
- for (const item of pending) {
5292
- if (item.sig === sig) return;
5293
- }
5294
- pending.push({ id: nextId++, event: stamped, sig });
5295
- enforceSizeCap();
5296
- persist();
5297
- if (retryTimer === void 0) flush();
5298
- } catch {
5299
- }
5300
- };
5301
- const notifyOnline = () => {
5302
- attempt = 0;
5303
- clearRetry();
5304
- flush();
5305
- };
5306
- const size = () => pending.length;
5307
- return { enqueue, flush, notifyOnline, size };
5308
5376
  };
5309
-
5310
- // src/session-analytics/useLifecycleEvents.ts
5377
+ var wireLifecycleEvents = (opts) => {
5378
+ var _a;
5379
+ const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5380
+ reportFirstOpen({ ...opts, sessionId });
5381
+ reportSessionStart({
5382
+ target: opts.target,
5383
+ sink: opts.sink,
5384
+ sessionId,
5385
+ userId: opts.userId,
5386
+ deviceKey: opts.deviceKey,
5387
+ sessionCount: opts.sessionCount,
5388
+ appVersion: opts.appVersion,
5389
+ platform: opts.platform,
5390
+ device: opts.device,
5391
+ meta: opts.meta
5392
+ });
5393
+ };
5311
5394
  var useLifecycleEvents = (config, options = {}) => {
5312
5395
  const latest = useRef({ config, options });
5313
5396
  latest.current = { config, options };
5314
5397
  const queueRef = useRef(void 0);
5315
5398
  useEffect(() => {
5399
+ let cancelled = false;
5316
5400
  const resolveSink = () => {
5317
5401
  var _a, _b;
5318
5402
  const { config: cfg, options: opts } = latest.current;
@@ -5333,7 +5417,7 @@ var useLifecycleEvents = (config, options = {}) => {
5333
5417
  var _a;
5334
5418
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a = cfg.apiKey) != null ? _a : "" } : void 0;
5335
5419
  };
5336
- const resolveDeviceKey = (cfg, opts) => {
5420
+ const resolveDeviceKey = (cfg, opts, autoDeviceKey) => {
5337
5421
  var _a;
5338
5422
  const host = (_a = resolveIdentity({
5339
5423
  value: opts.deviceKey,
@@ -5342,11 +5426,22 @@ var useLifecycleEvents = (config, options = {}) => {
5342
5426
  scope: cfg == null ? void 0 : cfg.appId
5343
5427
  })) == null ? void 0 : _a.value;
5344
5428
  if (host) return host;
5345
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5346
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5429
+ return autoDeviceKey;
5430
+ };
5431
+ const openAutoDeviceKey = (fire) => {
5432
+ const { config: cfg, options: opts } = latest.current;
5433
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5434
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5435
+ fire(void 0);
5436
+ return;
5437
+ }
5438
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5439
+ if (cancelled) return;
5440
+ fire((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5441
+ });
5347
5442
  };
5348
5443
  const mountOpenSessionId = makeSessionId();
5349
- const fireSession = (sessionId) => {
5444
+ const fireSession = (sessionId, autoDeviceKey) => {
5350
5445
  var _a;
5351
5446
  const { config: cfg, options: opts } = latest.current;
5352
5447
  if (opts.enabled === false) return;
@@ -5358,7 +5453,7 @@ var useLifecycleEvents = (config, options = {}) => {
5358
5453
  sink: resolveSink(),
5359
5454
  sessionId,
5360
5455
  userId: opts.userId,
5361
- deviceKey: resolveDeviceKey(cfg, opts),
5456
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5362
5457
  sessionCount: opts.sessionCount,
5363
5458
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5364
5459
  platform: Platform.OS,
@@ -5366,9 +5461,9 @@ var useLifecycleEvents = (config, options = {}) => {
5366
5461
  meta: opts.meta
5367
5462
  });
5368
5463
  };
5369
- const fireMountOpen = () => {
5464
+ const fireMountOpen = (autoDeviceKey) => {
5370
5465
  var _a;
5371
- fireSession(mountOpenSessionId);
5466
+ fireSession(mountOpenSessionId, autoDeviceKey);
5372
5467
  const { config: cfg, options: opts } = latest.current;
5373
5468
  if (opts.enabled === false) return;
5374
5469
  const device = collectDeviceContext();
@@ -5380,7 +5475,7 @@ var useLifecycleEvents = (config, options = {}) => {
5380
5475
  storage: cfg == null ? void 0 : cfg.storage,
5381
5476
  appId: cfg == null ? void 0 : cfg.appId,
5382
5477
  userId: opts.userId,
5383
- deviceKey: resolveDeviceKey(cfg, opts),
5478
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5384
5479
  sessionCount: opts.sessionCount,
5385
5480
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5386
5481
  platform: Platform.OS,
@@ -5388,16 +5483,7 @@ var useLifecycleEvents = (config, options = {}) => {
5388
5483
  meta: opts.meta
5389
5484
  });
5390
5485
  };
5391
- let cancelled = false;
5392
- const { config: mountCfg, options: mountOpts } = latest.current;
5393
- const hostKey = typeof mountOpts.deviceKey === "string" && mountOpts.deviceKey.trim() ? mountOpts.deviceKey : void 0;
5394
- if (!hostKey && (mountCfg == null ? void 0 : mountCfg.storage)) {
5395
- void hydrateAutoDeviceKey({ appId: mountCfg.appId, storage: mountCfg.storage }).then(() => {
5396
- if (!cancelled) fireMountOpen();
5397
- });
5398
- } else {
5399
- fireMountOpen();
5400
- }
5486
+ openAutoDeviceKey(fireMountOpen);
5401
5487
  let backgroundedAt = null;
5402
5488
  const onChange = (state) => {
5403
5489
  if (state === "background" || state === "inactive") {
@@ -5407,13 +5493,18 @@ var useLifecycleEvents = (config, options = {}) => {
5407
5493
  if (state === "active") {
5408
5494
  const since = backgroundedAt;
5409
5495
  backgroundedAt = null;
5410
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fireSession();
5496
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) {
5497
+ openAutoDeviceKey((autoDeviceKey) => fireSession(void 0, autoDeviceKey));
5498
+ }
5411
5499
  }
5412
5500
  };
5413
5501
  const sub = AppState.addEventListener("change", onChange);
5414
5502
  return () => {
5503
+ var _a;
5415
5504
  cancelled = true;
5416
5505
  if (sub && typeof sub.remove === "function") sub.remove();
5506
+ (_a = queueRef.current) == null ? void 0 : _a.dispose();
5507
+ queueRef.current = void 0;
5417
5508
  };
5418
5509
  }, []);
5419
5510
  };