@wireai/activation 0.13.6 → 0.14.1

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 (47) hide show
  1. package/AGENTS.md +21 -9
  2. package/CHANGELOG.md +227 -1
  3. package/INTEGRATION_PROMPT.md +7 -4
  4. package/README.md +15 -2
  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-DngW-QoD.d.mts → currentSession-CUvTOchb.d.mts} +35 -6
  12. package/dist/{currentSession-C5976akx.d.ts → currentSession-CW_5Mq4O.d.ts} +35 -6
  13. package/dist/index.d.mts +31 -7
  14. package/dist/index.d.ts +31 -7
  15. package/dist/index.js +643 -539
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +643 -539
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/questionnaire/index.js +14 -6
  20. package/dist/questionnaire/index.js.map +1 -1
  21. package/dist/questionnaire/index.mjs +14 -6
  22. package/dist/questionnaire/index.mjs.map +1 -1
  23. package/dist/reviews/index.js +14 -6
  24. package/dist/reviews/index.js.map +1 -1
  25. package/dist/reviews/index.mjs +14 -6
  26. package/dist/reviews/index.mjs.map +1 -1
  27. package/llms.txt +2 -2
  28. package/package.json +1 -1
  29. package/src/OnboardingFlow.tsx +4 -3
  30. package/src/WireOnboarding.tsx +48 -8
  31. package/src/activation/useWireActivation.ts +14 -1
  32. package/src/activation/wireActivation.ts +68 -2
  33. package/src/analytics/analyticsFacade.ts +24 -0
  34. package/src/analytics/currentSession.ts +3 -2
  35. package/src/analytics/eventQueue.ts +106 -11
  36. package/src/analytics/reportClientEvent.ts +31 -7
  37. package/src/analytics/useAnalytics.ts +17 -0
  38. package/src/context/deviceId.ts +10 -3
  39. package/src/permissions/permissionMemory.ts +41 -5
  40. package/src/reviews/runtime.ts +75 -23
  41. package/src/session/persistedSession.ts +32 -8
  42. package/src/session-analytics/lifecycle.ts +26 -5
  43. package/src/session-analytics/reportSessionStart.ts +14 -10
  44. package/src/session-analytics/useLifecycleEvents.ts +70 -32
  45. package/src/session-analytics/useSessionStart.ts +57 -15
  46. package/src/types.ts +16 -6
  47. package/src/utils/readPlan.ts +8 -5
package/dist/index.mjs CHANGED
@@ -1228,20 +1228,21 @@ var reportClientEvents = (target, events) => {
1228
1228
  }
1229
1229
  };
1230
1230
  var reportClientEvent = (target, event) => reportClientEvents(target, [event]);
1231
- var reportClientEventsAwait = async (target, events) => {
1231
+ var reportClientEventsAwait = async (target, events) => await reportClientEventsOutcome(target, events) === "delivered";
1232
+ var reportClientEventsOutcome = async (target, events) => {
1232
1233
  try {
1233
1234
  const req = buildEventsRequest(target, events);
1234
- if (!req) return false;
1235
+ if (!req) return "refused";
1235
1236
  const res = await fetch(req.url, req.init);
1236
- if (!res || !res.ok) return false;
1237
+ if (!res || !res.ok) return "unreachable";
1237
1238
  const ack = await readEventsAck(res);
1238
- if (!ack || ack.skipped <= 0) return true;
1239
+ if (!ack || ack.skipped <= 0) return "delivered";
1239
1240
  if (typeof __DEV__ !== "undefined" && __DEV__ && typeof console !== "undefined" && console.warn) {
1240
1241
  console.warn(describeDiscarded(ack));
1241
1242
  }
1242
- return false;
1243
+ return "refused";
1243
1244
  } catch {
1244
- return false;
1245
+ return "unreachable";
1245
1246
  }
1246
1247
  };
1247
1248
  var reportClientEventAwait = (target, event) => reportClientEventsAwait(target, [event]);
@@ -3616,10 +3617,11 @@ var ensureCurrentSessionId = () => {
3616
3617
  var DEFAULT_SESSION_TTL_MS = 36e5;
3617
3618
  var READ_TIMEOUT_MS = 1500;
3618
3619
  var sessionStorageKey = (appId) => `wireai:session:${appId}`;
3620
+ var READ_TIMED_OUT = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
3619
3621
  var withTimeout = (p, ms) => {
3620
3622
  let timer;
3621
3623
  const timeout = new Promise((resolve) => {
3622
- timer = setTimeout(() => resolve(void 0), ms);
3624
+ timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);
3623
3625
  });
3624
3626
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
3625
3627
  };
@@ -3635,12 +3637,14 @@ var parsePersisted = (raw) => {
3635
3637
  return void 0;
3636
3638
  };
3637
3639
  var loadPersistedSession = async (storage, key, ttlMs = DEFAULT_SESSION_TTL_MS) => {
3638
- let stored;
3640
+ let raw;
3639
3641
  try {
3640
- stored = parsePersisted(await withTimeout(storage.getItem(key), READ_TIMEOUT_MS));
3642
+ raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
3641
3643
  } catch {
3642
- stored = void 0;
3644
+ raw = null;
3643
3645
  }
3646
+ if (raw === READ_TIMED_OUT) return { id: makeSessionId(), resumed: false };
3647
+ const stored = parsePersisted(raw);
3644
3648
  if (stored && Date.now() - stored.ts < ttlMs) {
3645
3649
  return { id: stored.id, resumed: true };
3646
3650
  }
@@ -3650,7 +3654,9 @@ var loadPersistedSession = async (storage, key, ttlMs = DEFAULT_SESSION_TTL_MS)
3650
3654
  };
3651
3655
  var peekPersistedSession = async (storage, key) => {
3652
3656
  try {
3653
- 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);
3654
3660
  } catch {
3655
3661
  return void 0;
3656
3662
  }
@@ -3801,13 +3807,19 @@ var readSettledPermissions = (raw, sessionId) => {
3801
3807
  return [];
3802
3808
  }
3803
3809
  };
3804
- var loadSettledPermissions = async (storage, key, sessionId) => {
3810
+ var loadSettledPermissionsOutcome = async (storage, key, sessionId) => {
3805
3811
  try {
3806
- 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 { status: "unknown" };
3814
+ return { status: "read", ids: readSettledPermissions(raw, sessionId) };
3807
3815
  } catch {
3808
- return [];
3816
+ return { status: "unknown" };
3809
3817
  }
3810
3818
  };
3819
+ var loadSettledPermissions = async (storage, key, sessionId) => {
3820
+ const outcome = await loadSettledPermissionsOutcome(storage, key, sessionId);
3821
+ return outcome.status === "read" ? outcome.ids : [];
3822
+ };
3811
3823
  var saveSettledPermissions = (storage, key, sessionId, ids) => {
3812
3824
  try {
3813
3825
  const record = { sessionId, ids: [...ids] };
@@ -3912,7 +3924,7 @@ var WireOnboarding = ({
3912
3924
  useEffect(() => {
3913
3925
  if (!warnMissingJoinKey) return;
3914
3926
  warnInDev(
3915
- "[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
3927
+ "[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
3916
3928
  );
3917
3929
  }, [warnMissingJoinKey, autoJoinReason]);
3918
3930
  const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
@@ -3962,16 +3974,33 @@ var WireOnboarding = ({
3962
3974
  const sessionId = (_c = session == null ? void 0 : session.id) != null ? _c : "";
3963
3975
  const wantsPermissionMemory = Boolean(storage) && ((_d = permissionScreens == null ? void 0 : permissionScreens.length) != null ? _d : 0) > 0;
3964
3976
  const [settledPermissions, setSettledPermissions] = useState(void 0);
3977
+ const [permissionMemoryUnreadable, setPermissionMemoryUnreadable] = useState(false);
3965
3978
  useEffect(() => {
3966
- if (!wantsPermissionMemory || !storage || !sessionId || settledPermissions !== void 0) return;
3979
+ if (!wantsPermissionMemory || !storage || !sessionId) return;
3980
+ if (settledPermissions !== void 0 || permissionMemoryUnreadable) return;
3967
3981
  let cancelled = false;
3968
- void loadSettledPermissions(storage, permissionsKey, sessionId).then((ids) => {
3969
- if (!cancelled) setSettledPermissions(ids);
3982
+ void loadSettledPermissionsOutcome(storage, permissionsKey, sessionId).then((outcome) => {
3983
+ if (cancelled) return;
3984
+ if (outcome.status === "read") {
3985
+ setSettledPermissions(outcome.ids);
3986
+ return;
3987
+ }
3988
+ setPermissionMemoryUnreadable(true);
3989
+ warnInDev(
3990
+ "[wireai] the permission-screen memory could not be read (the storage adapter timed out or threw), so permission screens are suppressed for this session rather than re-asking a permission the OS grants once. Check the `storage` adapter passed to WireOnboarding."
3991
+ );
3970
3992
  });
3971
3993
  return () => {
3972
3994
  cancelled = true;
3973
3995
  };
3974
- }, [wantsPermissionMemory, storage, permissionsKey, sessionId, settledPermissions]);
3996
+ }, [
3997
+ wantsPermissionMemory,
3998
+ storage,
3999
+ permissionsKey,
4000
+ sessionId,
4001
+ settledPermissions,
4002
+ permissionMemoryUnreadable
4003
+ ]);
3975
4004
  const settledPermissionsRef = useRef(void 0);
3976
4005
  settledPermissionsRef.current = settledPermissions;
3977
4006
  const handlePermissionSettled = useCallback(
@@ -4591,190 +4620,503 @@ var attributionMetadata = (a) => {
4591
4620
  return { attribution };
4592
4621
  };
4593
4622
 
4594
- // src/activation/revalidation.ts
4595
- var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4596
- "@wireai/activation:activationRevalidation"
4597
- );
4598
- var globalSlot2 = globalThis;
4599
- var store = () => {
4600
- const existing = globalSlot2[REVALIDATION_SLOT];
4623
+ // src/analytics/eventQueue.ts
4624
+ var DEFAULTS = {
4625
+ maxSize: 200,
4626
+ batchSize: 20,
4627
+ baseBackoffMs: 1e3,
4628
+ maxBackoffMs: 3e4,
4629
+ maxRetries: 6
4630
+ };
4631
+ var READ_TIMEOUT_MS3 = 1500;
4632
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
4633
+ var queueKeyGlobal = globalThis;
4634
+ var claimedQueueKeys = () => {
4635
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
4601
4636
  if (existing) return existing;
4602
- const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4603
- globalSlot2[REVALIDATION_SLOT] = created;
4637
+ const created = /* @__PURE__ */ new Set();
4638
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
4604
4639
  return created;
4605
4640
  };
4606
- var bumpActivationRevalidation = () => {
4607
- const s = store();
4608
- s.version += 1;
4609
- for (const listener of Array.from(s.listeners)) {
4610
- try {
4611
- listener();
4612
- } catch {
4613
- }
4641
+ var claimQueueKey = (preferred, explicit) => {
4642
+ const claimed = claimedQueueKeys();
4643
+ if (explicit || !claimed.has(preferred)) {
4644
+ claimed.add(preferred);
4645
+ return preferred;
4614
4646
  }
4647
+ let ordinal = 2;
4648
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
4649
+ const key = `${preferred}#${ordinal}`;
4650
+ claimed.add(key);
4651
+ warnInDev(
4652
+ `[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.`
4653
+ );
4654
+ return key;
4615
4655
  };
4616
- var subscribeActivationRevalidation = (listener) => {
4617
- const s = store();
4618
- s.listeners.add(listener);
4619
- return () => {
4620
- s.listeners.delete(listener);
4621
- };
4622
- };
4623
- var getActivationRevalidationVersion = () => store().version;
4624
- var resetActivationRevalidation = () => {
4625
- const s = store();
4626
- s.version = 0;
4627
- s.listeners.clear();
4656
+ var releaseQueueKey = (key) => {
4657
+ claimedQueueKeys().delete(key);
4628
4658
  };
4629
-
4630
- // src/activation/wireActivation.ts
4631
- var clean = cleanString;
4632
- var createWireActivation = (config) => {
4633
- var _a, _b;
4634
- const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4635
- const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4636
- resolveIdentity({
4637
- value: explicitDeviceKey,
4638
- space: "device",
4639
- source: "host",
4640
- scope: config.appId
4659
+ var READ_TIMED_OUT2 = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
4660
+ var withTimeout3 = (p, ms) => {
4661
+ let timer;
4662
+ const timeout = new Promise((resolve) => {
4663
+ timer = setTimeout(() => resolve(READ_TIMED_OUT2), ms);
4641
4664
  });
4642
- const autoDeviceKeyOptions = {
4643
- appId: config.appId,
4644
- // An explicit key opts out of minting AND persisting (unchanged contract).
4645
- storage: explicitDeviceKey ? void 0 : config.storage
4646
- };
4647
- if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4648
- const detectedAppVersion = detectAppVersion();
4649
- const applyContext = (event) => {
4650
- var _a2, _b2, _c;
4651
- const resolved = resolveUserContext(
4652
- // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4653
- {
4654
- ...(_a2 = config.userContext) != null ? _a2 : {},
4655
- deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4656
- },
4657
- { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4658
- );
4659
- if (resolved.userContext) {
4660
- event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4661
- }
4662
- if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4663
- };
4664
- const track = async (name, meta) => {
4665
- if (!clean(name)) return false;
4666
- const sessionId = ensureCurrentSessionId();
4667
- const event = {
4668
- event_type: "app_event",
4669
- session_id: sessionId,
4670
- question_key: name
4671
- };
4672
- if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
4673
- applyContext(event);
4674
- const ok = await reportClientEventAwait(target, event);
4675
- if (ok) bumpActivationRevalidation();
4676
- return ok;
4677
- };
4678
- return {
4679
- track,
4680
- get sessionId() {
4681
- return getCurrentSessionId();
4682
- },
4683
- subscribeRevalidation: subscribeActivationRevalidation,
4684
- getRevalidationVersion: getActivationRevalidationVersion
4685
- };
4686
- };
4687
- var useActivationRevalidation = () => useSyncExternalStore(
4688
- subscribeActivationRevalidation,
4689
- getActivationRevalidationVersion,
4690
- getActivationRevalidationVersion
4691
- );
4692
- var useWireActivation = (config) => {
4693
- var _a;
4694
- const ref = useRef(void 0);
4695
- const prevKeys = useRef("");
4696
- const currentKeys = [
4697
- config.serverUrl,
4698
- config.apiKey,
4699
- config.appId,
4700
- config.deviceKey,
4701
- (_a = config.userContext) == null ? void 0 : _a.deviceKey
4702
- ].join("|");
4703
- if (!ref.current || prevKeys.current !== currentKeys) {
4704
- prevKeys.current = currentKeys;
4705
- ref.current = createWireActivation(config);
4706
- }
4707
- const revalidation = useActivationRevalidation();
4708
- return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
4709
- };
4710
-
4711
- // src/revenuecat/purchaseEvents.ts
4712
- var WIRE_PURCHASE_EVENTS = {
4713
- /** The paywall became visible (offerings loaded). */
4714
- paywallShown: "wire_paywall_shown",
4715
- /** The user tapped buy; the store sheet is about to open. */
4716
- checkoutStarted: "wire_checkout_started",
4717
- /** The store confirmed the purchase AND the entitlement is now active. */
4718
- purchased: "wire_purchase_completed",
4719
- /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
4720
- purchaseFailed: "wire_purchase_failed",
4721
- /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
4722
- restored: "wire_purchase_restored"
4723
- };
4724
- var PLAN_TIER_CONTEXT_KEY = "plan_tier";
4725
- var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
4726
- var activeEntitlement = (info, entitlementId) => {
4727
- var _a, _b;
4728
- const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
4729
- const found = active == null ? void 0 : active[entitlementId];
4730
- const entitlement = asRecord(found);
4731
- if (!entitlement) return void 0;
4732
- return entitlement.isActive === false ? void 0 : entitlement;
4665
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4733
4666
  };
4734
- var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
4735
- var resolvePlanTier = (info, entitlementId) => {
4736
- const entitlement = activeEntitlement(info, entitlementId);
4737
- if (!entitlement) return "free";
4738
- return isTrial(entitlement) ? "trial" : "paid";
4667
+ var unrefTimer = (timer) => {
4668
+ const t = timer;
4669
+ if (typeof t.unref === "function") t.unref();
4739
4670
  };
4740
- var describePackage = (pkg) => {
4741
- const props = {};
4742
- if (!pkg) return props;
4743
- if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
4744
- if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
4745
- if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
4746
- const product = asRecord(pkg.product);
4747
- if (product) {
4748
- if (typeof product.identifier === "string") props.product_id = product.identifier;
4749
- if (typeof product.price === "number" && Number.isFinite(product.price)) {
4750
- props.price = product.price;
4671
+ var parsePersisted2 = (raw) => {
4672
+ if (!raw) return [];
4673
+ try {
4674
+ const parsed = JSON.parse(raw);
4675
+ if (!Array.isArray(parsed)) return [];
4676
+ const items = [];
4677
+ for (const entry of parsed) {
4678
+ if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
4679
+ items.push(entry);
4680
+ }
4751
4681
  }
4752
- if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
4682
+ return items;
4683
+ } catch {
4684
+ return [];
4753
4685
  }
4754
- return props;
4755
4686
  };
4756
- var describeEntitlement = (info, entitlementId) => {
4757
- const entitlement = activeEntitlement(info, entitlementId);
4758
- const props = {
4759
- entitlement: entitlementId,
4760
- plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
4687
+ var createEventQueue = (options) => {
4688
+ var _a, _b, _c, _d, _e, _f, _g;
4689
+ const target = options.target;
4690
+ const storage = options.storage;
4691
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
4692
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4693
+ const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4694
+ const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4695
+ const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
4696
+ const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
4697
+ const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
4698
+ let pending = [];
4699
+ let nextId = 0;
4700
+ let flushing = false;
4701
+ let attempt = 0;
4702
+ let retryTimer;
4703
+ let disposed = false;
4704
+ let backlogUnread = storage !== void 0;
4705
+ const resolveEnvelope = () => {
4706
+ try {
4707
+ return typeof options.envelope === "function" ? options.envelope() : options.envelope;
4708
+ } catch {
4709
+ return void 0;
4710
+ }
4761
4711
  };
4762
- if (!entitlement) return props;
4763
- if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
4764
- props.is_trial = isTrial(entitlement);
4765
- if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
4766
- if (typeof entitlement.store === "string") props.store = entitlement.store;
4767
- if (typeof entitlement.productIdentifier === "string") {
4768
- props.product_id = entitlement.productIdentifier;
4769
- }
4770
- if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
4771
- return props;
4772
- };
4773
- var PURCHASE_CANCELLED_CODE = "1";
4774
- var isUserCancelled = (error) => {
4775
- const record = asRecord(error);
4776
- if (!record) return false;
4777
- if (record.userCancelled === true) return true;
4712
+ const stamp = (event) => {
4713
+ var _a2;
4714
+ const env = resolveEnvelope();
4715
+ const stamped = { ...event };
4716
+ if (stamped.ts === void 0) stamped.ts = Date.now();
4717
+ if (!env) return stamped;
4718
+ if (!stamped.device && env.device) stamped.device = env.device;
4719
+ if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
4720
+ const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
4721
+ if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
4722
+ if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
4723
+ if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
4724
+ if (Object.keys(uc).length > 0) stamped.user_context = uc;
4725
+ return stamped;
4726
+ };
4727
+ const persist = () => {
4728
+ if (!storage) return;
4729
+ if (disposed) return;
4730
+ if (backlogUnread) return;
4731
+ try {
4732
+ if (pending.length === 0) {
4733
+ void storage.removeItem(key).catch(() => {
4734
+ });
4735
+ return;
4736
+ }
4737
+ const payload = pending.map((item) => ({ id: item.id, event: item.event }));
4738
+ void storage.setItem(key, JSON.stringify(payload)).catch(() => {
4739
+ });
4740
+ } catch {
4741
+ }
4742
+ };
4743
+ const isCountedOpenEvent = (event) => typeof event.question_key === "string" && event.question_key.startsWith("app.");
4744
+ const enforceSizeCap = () => {
4745
+ let overflow = pending.length - maxSize;
4746
+ if (overflow <= 0) return;
4747
+ const kept = [];
4748
+ for (const item of pending) {
4749
+ if (overflow > 0 && !isCountedOpenEvent(item.event)) {
4750
+ overflow--;
4751
+ continue;
4752
+ }
4753
+ kept.push(item);
4754
+ }
4755
+ if (overflow > 0) kept.splice(0, overflow);
4756
+ pending = kept;
4757
+ };
4758
+ const safeSig = (event) => {
4759
+ try {
4760
+ return JSON.stringify(event);
4761
+ } catch {
4762
+ return `__nosig_${nextId}_${Math.random()}`;
4763
+ }
4764
+ };
4765
+ const mergePersisted = (persistedItems) => {
4766
+ if (persistedItems.length === 0) return;
4767
+ const seen = new Set(pending.map((item) => item.sig));
4768
+ const restored = [];
4769
+ for (const persisted of persistedItems) {
4770
+ const sig = safeSig(persisted.event);
4771
+ if (seen.has(sig)) continue;
4772
+ seen.add(sig);
4773
+ restored.push({ id: nextId++, event: persisted.event, sig });
4774
+ }
4775
+ if (restored.length === 0) return;
4776
+ pending = [...restored, ...pending];
4777
+ enforceSizeCap();
4778
+ persist();
4779
+ };
4780
+ const loadPromise = (async () => {
4781
+ if (!storage) return;
4782
+ try {
4783
+ const read = Promise.resolve(storage.getItem(key));
4784
+ read.catch(() => {
4785
+ });
4786
+ const raced = await withTimeout3(read, READ_TIMEOUT_MS3);
4787
+ if (raced === READ_TIMED_OUT2) {
4788
+ backlogUnread = true;
4789
+ warnInDev(
4790
+ `[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.`
4791
+ );
4792
+ void read.then((late) => {
4793
+ backlogUnread = false;
4794
+ mergePersisted(parsePersisted2(late));
4795
+ flush();
4796
+ }).catch(() => {
4797
+ backlogUnread = false;
4798
+ persist();
4799
+ });
4800
+ return;
4801
+ }
4802
+ backlogUnread = false;
4803
+ mergePersisted(parsePersisted2(raced));
4804
+ persist();
4805
+ } catch {
4806
+ backlogUnread = false;
4807
+ persist();
4808
+ }
4809
+ })();
4810
+ const postBatch = async (events) => {
4811
+ const req = buildEventsRequest(target, events);
4812
+ if (!req) return false;
4813
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
4814
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
4815
+ try {
4816
+ const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
4817
+ warnOnSkippedEvents(res);
4818
+ return !!(res && res.ok);
4819
+ } catch {
4820
+ return false;
4821
+ } finally {
4822
+ clearTimeout(timer);
4823
+ }
4824
+ };
4825
+ const clearRetry = () => {
4826
+ if (retryTimer !== void 0) {
4827
+ clearTimeout(retryTimer);
4828
+ retryTimer = void 0;
4829
+ }
4830
+ };
4831
+ const scheduleRetry = () => {
4832
+ if (attempt >= maxRetries) return;
4833
+ const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
4834
+ attempt++;
4835
+ clearRetry();
4836
+ retryTimer = setTimeout(() => {
4837
+ retryTimer = void 0;
4838
+ void drain();
4839
+ }, delay);
4840
+ unrefTimer(retryTimer);
4841
+ };
4842
+ const drain = async () => {
4843
+ if (disposed) return;
4844
+ try {
4845
+ await loadPromise;
4846
+ } catch {
4847
+ }
4848
+ if (disposed) return;
4849
+ if (flushing) return;
4850
+ flushing = true;
4851
+ try {
4852
+ while (pending.length > 0) {
4853
+ const batch = pending.slice(0, batchSize);
4854
+ const ok = await postBatch(batch.map((item) => item.event));
4855
+ if (!ok) {
4856
+ scheduleRetry();
4857
+ return;
4858
+ }
4859
+ const acked = new Set(batch.map((item) => item.id));
4860
+ pending = pending.filter((item) => !acked.has(item.id));
4861
+ persist();
4862
+ attempt = 0;
4863
+ clearRetry();
4864
+ }
4865
+ } finally {
4866
+ flushing = false;
4867
+ }
4868
+ };
4869
+ const flush = () => {
4870
+ try {
4871
+ void drain();
4872
+ } catch {
4873
+ }
4874
+ };
4875
+ const enqueue = (event) => {
4876
+ if (disposed) return;
4877
+ try {
4878
+ const stamped = stamp(event);
4879
+ const sig = safeSig(stamped);
4880
+ for (const item of pending) {
4881
+ if (item.sig === sig) return;
4882
+ }
4883
+ pending.push({ id: nextId++, event: stamped, sig });
4884
+ enforceSizeCap();
4885
+ persist();
4886
+ if (retryTimer === void 0) flush();
4887
+ } catch {
4888
+ }
4889
+ };
4890
+ const notifyOnline = () => {
4891
+ attempt = 0;
4892
+ clearRetry();
4893
+ flush();
4894
+ };
4895
+ const size = () => pending.length;
4896
+ const dispose = () => {
4897
+ if (disposed) return;
4898
+ disposed = true;
4899
+ clearRetry();
4900
+ if (storage) releaseQueueKey(key);
4901
+ };
4902
+ return { enqueue, flush, notifyOnline, size, dispose };
4903
+ };
4904
+
4905
+ // src/activation/revalidation.ts
4906
+ var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4907
+ "@wireai/activation:activationRevalidation"
4908
+ );
4909
+ var globalSlot2 = globalThis;
4910
+ var store = () => {
4911
+ const existing = globalSlot2[REVALIDATION_SLOT];
4912
+ if (existing) return existing;
4913
+ const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4914
+ globalSlot2[REVALIDATION_SLOT] = created;
4915
+ return created;
4916
+ };
4917
+ var bumpActivationRevalidation = () => {
4918
+ const s = store();
4919
+ s.version += 1;
4920
+ for (const listener of Array.from(s.listeners)) {
4921
+ try {
4922
+ listener();
4923
+ } catch {
4924
+ }
4925
+ }
4926
+ };
4927
+ var subscribeActivationRevalidation = (listener) => {
4928
+ const s = store();
4929
+ s.listeners.add(listener);
4930
+ return () => {
4931
+ s.listeners.delete(listener);
4932
+ };
4933
+ };
4934
+ var getActivationRevalidationVersion = () => store().version;
4935
+ var resetActivationRevalidation = () => {
4936
+ const s = store();
4937
+ s.version = 0;
4938
+ s.listeners.clear();
4939
+ };
4940
+
4941
+ // src/activation/wireActivation.ts
4942
+ var clean = cleanString;
4943
+ var createWireActivation = (config) => {
4944
+ var _a, _b;
4945
+ const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4946
+ const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4947
+ resolveIdentity({
4948
+ value: explicitDeviceKey,
4949
+ space: "device",
4950
+ source: "host",
4951
+ scope: config.appId
4952
+ });
4953
+ const autoDeviceKeyOptions = {
4954
+ appId: config.appId,
4955
+ // An explicit key opts out of minting AND persisting (unchanged contract).
4956
+ storage: explicitDeviceKey ? void 0 : config.storage
4957
+ };
4958
+ if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4959
+ const detectedAppVersion = detectAppVersion();
4960
+ const applyContext = (event) => {
4961
+ var _a2, _b2, _c;
4962
+ const resolved = resolveUserContext(
4963
+ // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4964
+ {
4965
+ ...(_a2 = config.userContext) != null ? _a2 : {},
4966
+ deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4967
+ },
4968
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4969
+ );
4970
+ if (resolved.userContext) {
4971
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4972
+ }
4973
+ if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4974
+ };
4975
+ let durability;
4976
+ const bufferFailedEvent = (event) => {
4977
+ var _a2;
4978
+ if (config.sink) {
4979
+ config.sink(event);
4980
+ return;
4981
+ }
4982
+ if (!durability) {
4983
+ durability = createEventQueue({
4984
+ target,
4985
+ storage: config.storage,
4986
+ storageKey: `wireai:evtq:activation:${(_a2 = config.appId) != null ? _a2 : "default"}`
4987
+ });
4988
+ }
4989
+ durability.enqueue(event);
4990
+ };
4991
+ const track = async (name, meta) => {
4992
+ if (!clean(name)) return false;
4993
+ const sessionId = ensureCurrentSessionId();
4994
+ const event = {
4995
+ event_type: "app_event",
4996
+ session_id: sessionId,
4997
+ question_key: name
4998
+ };
4999
+ if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
5000
+ applyContext(event);
5001
+ const outcome = await reportClientEventsOutcome(target, [event]);
5002
+ const ok = outcome === "delivered";
5003
+ if (ok) bumpActivationRevalidation();
5004
+ if (outcome === "unreachable") bufferFailedEvent(event);
5005
+ return ok;
5006
+ };
5007
+ return {
5008
+ track,
5009
+ get sessionId() {
5010
+ return getCurrentSessionId();
5011
+ },
5012
+ subscribeRevalidation: subscribeActivationRevalidation,
5013
+ getRevalidationVersion: getActivationRevalidationVersion,
5014
+ dispose: () => {
5015
+ durability == null ? void 0 : durability.dispose();
5016
+ durability = void 0;
5017
+ }
5018
+ };
5019
+ };
5020
+ var useActivationRevalidation = () => useSyncExternalStore(
5021
+ subscribeActivationRevalidation,
5022
+ getActivationRevalidationVersion,
5023
+ getActivationRevalidationVersion
5024
+ );
5025
+ var useWireActivation = (config) => {
5026
+ var _a, _b;
5027
+ const ref = useRef(void 0);
5028
+ const prevKeys = useRef("");
5029
+ const currentKeys = [
5030
+ config.serverUrl,
5031
+ config.apiKey,
5032
+ config.appId,
5033
+ config.deviceKey,
5034
+ (_a = config.userContext) == null ? void 0 : _a.deviceKey
5035
+ ].join("|");
5036
+ if (!ref.current || prevKeys.current !== currentKeys) {
5037
+ prevKeys.current = currentKeys;
5038
+ (_b = ref.current) == null ? void 0 : _b.dispose();
5039
+ ref.current = createWireActivation(config);
5040
+ }
5041
+ useEffect(
5042
+ () => () => {
5043
+ var _a2;
5044
+ (_a2 = ref.current) == null ? void 0 : _a2.dispose();
5045
+ ref.current = void 0;
5046
+ },
5047
+ []
5048
+ );
5049
+ const revalidation = useActivationRevalidation();
5050
+ return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
5051
+ };
5052
+
5053
+ // src/revenuecat/purchaseEvents.ts
5054
+ var WIRE_PURCHASE_EVENTS = {
5055
+ /** The paywall became visible (offerings loaded). */
5056
+ paywallShown: "wire_paywall_shown",
5057
+ /** The user tapped buy; the store sheet is about to open. */
5058
+ checkoutStarted: "wire_checkout_started",
5059
+ /** The store confirmed the purchase AND the entitlement is now active. */
5060
+ purchased: "wire_purchase_completed",
5061
+ /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
5062
+ purchaseFailed: "wire_purchase_failed",
5063
+ /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
5064
+ restored: "wire_purchase_restored"
5065
+ };
5066
+ var PLAN_TIER_CONTEXT_KEY = "plan_tier";
5067
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
5068
+ var activeEntitlement = (info, entitlementId) => {
5069
+ var _a, _b;
5070
+ const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
5071
+ const found = active == null ? void 0 : active[entitlementId];
5072
+ const entitlement = asRecord(found);
5073
+ if (!entitlement) return void 0;
5074
+ return entitlement.isActive === false ? void 0 : entitlement;
5075
+ };
5076
+ var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
5077
+ var resolvePlanTier = (info, entitlementId) => {
5078
+ const entitlement = activeEntitlement(info, entitlementId);
5079
+ if (!entitlement) return "free";
5080
+ return isTrial(entitlement) ? "trial" : "paid";
5081
+ };
5082
+ var describePackage = (pkg) => {
5083
+ const props = {};
5084
+ if (!pkg) return props;
5085
+ if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
5086
+ if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
5087
+ if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
5088
+ const product = asRecord(pkg.product);
5089
+ if (product) {
5090
+ if (typeof product.identifier === "string") props.product_id = product.identifier;
5091
+ if (typeof product.price === "number" && Number.isFinite(product.price)) {
5092
+ props.price = product.price;
5093
+ }
5094
+ if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
5095
+ }
5096
+ return props;
5097
+ };
5098
+ var describeEntitlement = (info, entitlementId) => {
5099
+ const entitlement = activeEntitlement(info, entitlementId);
5100
+ const props = {
5101
+ entitlement: entitlementId,
5102
+ plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
5103
+ };
5104
+ if (!entitlement) return props;
5105
+ if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
5106
+ props.is_trial = isTrial(entitlement);
5107
+ if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
5108
+ if (typeof entitlement.store === "string") props.store = entitlement.store;
5109
+ if (typeof entitlement.productIdentifier === "string") {
5110
+ props.product_id = entitlement.productIdentifier;
5111
+ }
5112
+ if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
5113
+ return props;
5114
+ };
5115
+ var PURCHASE_CANCELLED_CODE = "1";
5116
+ var isUserCancelled = (error) => {
5117
+ const record = asRecord(error);
5118
+ if (!record) return false;
5119
+ if (record.userCancelled === true) return true;
4778
5120
  return record.code === PURCHASE_CANCELLED_CODE;
4779
5121
  };
4780
5122
  var describeFailure = (error) => {
@@ -4900,15 +5242,9 @@ var reportSessionStart = (opts) => {
4900
5242
  opts.sink(event);
4901
5243
  return;
4902
5244
  }
4903
- if (!(target == null ? void 0 : target.serverUrl)) return;
4904
- const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
4905
- const headers = { "Content-Type": "application/json" };
4906
- if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
4907
- void fetch(url, {
4908
- method: "POST",
4909
- headers,
4910
- body: JSON.stringify({ events: [event] })
4911
- }).then((res) => {
5245
+ const req = buildEventsRequest(target, [event]);
5246
+ if (!req) return;
5247
+ void fetch(req.url, req.init).then((res) => {
4912
5248
  warnOnSkippedEvents(res);
4913
5249
  }).catch(() => {
4914
5250
  });
@@ -4920,13 +5256,25 @@ var useSessionStart = (config, options = {}) => {
4920
5256
  const latest = useRef({ config, options });
4921
5257
  latest.current = { config, options };
4922
5258
  useEffect(() => {
4923
- const resolveDeviceKey = (cfg, opts) => {
5259
+ let cancelled = false;
5260
+ const resolveDeviceKey = (opts, autoDeviceKey) => {
4924
5261
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4925
5262
  if (host) return host;
4926
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4927
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5263
+ return autoDeviceKey;
4928
5264
  };
4929
- const fire = () => {
5265
+ const openAutoDeviceKey = (fireOpen) => {
5266
+ const { config: cfg, options: opts } = latest.current;
5267
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5268
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5269
+ fireOpen(void 0);
5270
+ return;
5271
+ }
5272
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5273
+ if (cancelled) return;
5274
+ fireOpen((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5275
+ });
5276
+ };
5277
+ const fire = (autoDeviceKey) => {
4930
5278
  var _a, _b;
4931
5279
  const { config: cfg, options: opts } = latest.current;
4932
5280
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return;
@@ -4938,7 +5286,7 @@ var useSessionStart = (config, options = {}) => {
4938
5286
  target,
4939
5287
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4940
5288
  userId: opts.userId,
4941
- deviceKey: resolveDeviceKey(cfg, opts),
5289
+ deviceKey: resolveDeviceKey(opts, autoDeviceKey),
4942
5290
  sessionCount: opts.sessionCount,
4943
5291
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4944
5292
  platform: Platform.OS,
@@ -4946,7 +5294,7 @@ var useSessionStart = (config, options = {}) => {
4946
5294
  meta: opts.meta
4947
5295
  });
4948
5296
  };
4949
- fire();
5297
+ openAutoDeviceKey(fire);
4950
5298
  let backgroundedAt = null;
4951
5299
  const onChange = (state) => {
4952
5300
  if (state === "background" || state === "inactive") {
@@ -4956,11 +5304,12 @@ var useSessionStart = (config, options = {}) => {
4956
5304
  if (state === "active") {
4957
5305
  const since = backgroundedAt;
4958
5306
  backgroundedAt = null;
4959
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fire();
5307
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) openAutoDeviceKey(fire);
4960
5308
  }
4961
5309
  };
4962
5310
  const sub = AppState.addEventListener("change", onChange);
4963
5311
  return () => {
5312
+ cancelled = true;
4964
5313
  if (sub && typeof sub.remove === "function") sub.remove();
4965
5314
  };
4966
5315
  }, []);
@@ -4969,11 +5318,12 @@ var useSessionStart = (config, options = {}) => {
4969
5318
  // src/session-analytics/lifecycle.ts
4970
5319
  var FIRST_OPEN_EVENT = "app.first_open";
4971
5320
  var firstOpenStorageKey = (appId) => `wireai:first_open:${appId}`;
4972
- var READ_TIMEOUT_MS3 = 1500;
4973
- var withTimeout3 = (p, ms) => {
5321
+ var READ_TIMEOUT_MS4 = 1500;
5322
+ var READ_TIMED_OUT3 = /* @__PURE__ */ Symbol("wireai:first-open-read-timeout");
5323
+ var withTimeout4 = (p, ms) => {
4974
5324
  let timer;
4975
5325
  const timeout = new Promise((resolve) => {
4976
- timer = setTimeout(() => resolve(void 0), ms);
5326
+ timer = setTimeout(() => resolve(READ_TIMED_OUT3), ms);
4977
5327
  });
4978
5328
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4979
5329
  };
@@ -4992,335 +5342,82 @@ var buildLifecycleEvent = (questionKey, opts) => {
4992
5342
  if (opts.appVersion) userContext.app_version = opts.appVersion;
4993
5343
  if (opts.platform) userContext.platform = opts.platform;
4994
5344
  const event = {
4995
- event_type: "app_event",
4996
- question_key: questionKey,
4997
- session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
4998
- };
4999
- const userId = sanitizeUserId(opts.userId);
5000
- if (userId) event.user_id = userId;
5001
- if (Object.keys(userContext).length > 0) event.user_context = userContext;
5002
- if (opts.device) event.device = opts.device;
5003
- if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
5004
- return event;
5005
- };
5006
- var routeLifecycleEvent = (event, opts) => {
5007
- try {
5008
- if (opts.sink) {
5009
- opts.sink(event);
5010
- return;
5011
- }
5012
- const req = buildEventsRequest(opts.target, [event]);
5013
- if (!req) return;
5014
- void fetch(req.url, req.init).catch(() => {
5015
- });
5016
- } catch {
5017
- }
5018
- };
5019
- var emitFirstOpen = (opts) => {
5020
- routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5021
- };
5022
- var reportFirstOpen = (opts) => {
5023
- var _a;
5024
- const appId = (_a = opts.appId) != null ? _a : "default";
5025
- if (_firstOpenLatched.has(appId)) return;
5026
- _firstOpenLatched.add(appId);
5027
- const storage = opts.storage;
5028
- if (!storage) {
5029
- emitFirstOpen(opts);
5030
- return;
5031
- }
5032
- const key = firstOpenStorageKey(appId);
5033
- void (async () => {
5034
- try {
5035
- const seen = await withTimeout3(storage.getItem(key), READ_TIMEOUT_MS3);
5036
- if (seen) return;
5037
- emitFirstOpen(opts);
5038
- try {
5039
- void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5040
- });
5041
- } catch {
5042
- }
5043
- } catch {
5044
- emitFirstOpen(opts);
5045
- }
5046
- })();
5047
- };
5048
- var wireLifecycleEvents = (opts) => {
5049
- var _a;
5050
- const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5051
- reportFirstOpen({ ...opts, sessionId });
5052
- reportSessionStart({
5053
- target: opts.target,
5054
- sink: opts.sink,
5055
- sessionId,
5056
- userId: opts.userId,
5057
- deviceKey: opts.deviceKey,
5058
- sessionCount: opts.sessionCount,
5059
- appVersion: opts.appVersion,
5060
- platform: opts.platform,
5061
- device: opts.device,
5062
- meta: opts.meta
5063
- });
5064
- };
5065
-
5066
- // src/analytics/eventQueue.ts
5067
- var DEFAULTS = {
5068
- maxSize: 200,
5069
- batchSize: 20,
5070
- baseBackoffMs: 1e3,
5071
- maxBackoffMs: 3e4,
5072
- maxRetries: 6
5073
- };
5074
- var READ_TIMEOUT_MS4 = 1500;
5075
- var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
5076
- var queueKeyGlobal = globalThis;
5077
- var claimedQueueKeys = () => {
5078
- const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
5079
- if (existing) return existing;
5080
- const created = /* @__PURE__ */ new Set();
5081
- queueKeyGlobal[QUEUE_KEY_SLOT] = created;
5082
- return created;
5083
- };
5084
- var claimQueueKey = (preferred, explicit) => {
5085
- const claimed = claimedQueueKeys();
5086
- if (explicit || !claimed.has(preferred)) {
5087
- claimed.add(preferred);
5088
- return preferred;
5089
- }
5090
- let ordinal = 2;
5091
- while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
5092
- const key = `${preferred}#${ordinal}`;
5093
- claimed.add(key);
5094
- warnInDev(
5095
- `[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.`
5096
- );
5097
- return key;
5098
- };
5099
- var READ_TIMED_OUT = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
5100
- var withTimeout4 = (p, ms) => {
5101
- let timer;
5102
- const timeout = new Promise((resolve) => {
5103
- timer = setTimeout(() => resolve(READ_TIMED_OUT), ms);
5104
- });
5105
- return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
5106
- };
5107
- var unrefTimer = (timer) => {
5108
- const t = timer;
5109
- if (typeof t.unref === "function") t.unref();
5345
+ event_type: "app_event",
5346
+ question_key: questionKey,
5347
+ session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
5348
+ };
5349
+ const userId = sanitizeUserId(opts.userId);
5350
+ if (userId) event.user_id = userId;
5351
+ if (Object.keys(userContext).length > 0) event.user_context = userContext;
5352
+ if (opts.device) event.device = opts.device;
5353
+ if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
5354
+ return event;
5110
5355
  };
5111
- var parsePersisted2 = (raw) => {
5112
- if (!raw) return [];
5356
+ var routeLifecycleEvent = (event, opts) => {
5113
5357
  try {
5114
- const parsed = JSON.parse(raw);
5115
- if (!Array.isArray(parsed)) return [];
5116
- const items = [];
5117
- for (const entry of parsed) {
5118
- if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
5119
- items.push(entry);
5120
- }
5358
+ if (opts.sink) {
5359
+ opts.sink(event);
5360
+ return;
5121
5361
  }
5122
- return items;
5362
+ const req = buildEventsRequest(opts.target, [event]);
5363
+ if (!req) return;
5364
+ void fetch(req.url, req.init).catch(() => {
5365
+ });
5123
5366
  } catch {
5124
- return [];
5125
5367
  }
5126
5368
  };
5127
- var createEventQueue = (options) => {
5128
- var _a, _b, _c, _d, _e, _f, _g;
5129
- const target = options.target;
5130
- const storage = options.storage;
5131
- const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
5132
- const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
5133
- const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
5134
- const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
5135
- const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
5136
- const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
5137
- const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
5138
- let pending = [];
5139
- let nextId = 0;
5140
- let flushing = false;
5141
- let attempt = 0;
5142
- let retryTimer;
5143
- let backlogUnread = false;
5144
- const resolveEnvelope = () => {
5145
- try {
5146
- return typeof options.envelope === "function" ? options.envelope() : options.envelope;
5147
- } catch {
5148
- return void 0;
5149
- }
5150
- };
5151
- const stamp = (event) => {
5152
- var _a2;
5153
- const env = resolveEnvelope();
5154
- const stamped = { ...event };
5155
- if (stamped.ts === void 0) stamped.ts = Date.now();
5156
- if (!env) return stamped;
5157
- if (!stamped.device && env.device) stamped.device = env.device;
5158
- if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
5159
- const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
5160
- if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
5161
- if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
5162
- if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
5163
- if (Object.keys(uc).length > 0) stamped.user_context = uc;
5164
- return stamped;
5165
- };
5166
- const persist = () => {
5167
- if (!storage) return;
5168
- if (backlogUnread) return;
5169
- try {
5170
- if (pending.length === 0) {
5171
- void storage.removeItem(key).catch(() => {
5172
- });
5173
- return;
5174
- }
5175
- const payload = pending.map((item) => ({ id: item.id, event: item.event }));
5176
- void storage.setItem(key, JSON.stringify(payload)).catch(() => {
5177
- });
5178
- } catch {
5179
- }
5180
- };
5181
- const enforceSizeCap = () => {
5182
- if (pending.length > maxSize) pending.splice(0, pending.length - maxSize);
5183
- };
5184
- const safeSig = (event) => {
5185
- try {
5186
- return JSON.stringify(event);
5187
- } catch {
5188
- return `__nosig_${nextId}_${Math.random()}`;
5189
- }
5190
- };
5191
- const mergePersisted = (persistedItems) => {
5192
- if (persistedItems.length === 0) return;
5193
- const seen = new Set(pending.map((item) => item.sig));
5194
- const restored = [];
5195
- for (const persisted of persistedItems) {
5196
- const sig = safeSig(persisted.event);
5197
- if (seen.has(sig)) continue;
5198
- seen.add(sig);
5199
- restored.push({ id: nextId++, event: persisted.event, sig });
5200
- }
5201
- if (restored.length === 0) return;
5202
- pending = [...restored, ...pending];
5203
- enforceSizeCap();
5204
- persist();
5205
- };
5206
- const loadPromise = (async () => {
5207
- if (!storage) return;
5369
+ var emitFirstOpen = (opts) => {
5370
+ routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5371
+ };
5372
+ var reportFirstOpen = (opts) => {
5373
+ var _a;
5374
+ const appId = (_a = opts.appId) != null ? _a : "default";
5375
+ if (_firstOpenLatched.has(appId)) return;
5376
+ _firstOpenLatched.add(appId);
5377
+ const storage = opts.storage;
5378
+ if (!storage) {
5379
+ emitFirstOpen(opts);
5380
+ return;
5381
+ }
5382
+ const key = firstOpenStorageKey(appId);
5383
+ void (async () => {
5208
5384
  try {
5209
- const read = Promise.resolve(storage.getItem(key));
5210
- read.catch(() => {
5211
- });
5212
- const raced = await withTimeout4(read, READ_TIMEOUT_MS4);
5213
- if (raced === READ_TIMED_OUT) {
5214
- backlogUnread = true;
5215
- warnInDev(
5216
- `[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.`
5217
- );
5218
- void read.then((late) => {
5219
- backlogUnread = false;
5220
- mergePersisted(parsePersisted2(late));
5221
- flush();
5222
- }).catch(() => {
5223
- backlogUnread = false;
5224
- persist();
5385
+ const seen = await withTimeout4(storage.getItem(key), READ_TIMEOUT_MS4);
5386
+ if (seen === READ_TIMED_OUT3) return;
5387
+ if (seen) return;
5388
+ emitFirstOpen(opts);
5389
+ try {
5390
+ void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5225
5391
  });
5226
- return;
5392
+ } catch {
5227
5393
  }
5228
- mergePersisted(parsePersisted2(raced));
5229
5394
  } catch {
5230
5395
  }
5231
5396
  })();
5232
- const postBatch = async (events) => {
5233
- const req = buildEventsRequest(target, events);
5234
- if (!req) return false;
5235
- const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
5236
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
5237
- try {
5238
- const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
5239
- warnOnSkippedEvents(res);
5240
- return !!(res && res.ok);
5241
- } catch {
5242
- return false;
5243
- } finally {
5244
- clearTimeout(timer);
5245
- }
5246
- };
5247
- const clearRetry = () => {
5248
- if (retryTimer !== void 0) {
5249
- clearTimeout(retryTimer);
5250
- retryTimer = void 0;
5251
- }
5252
- };
5253
- const scheduleRetry = () => {
5254
- if (attempt >= maxRetries) return;
5255
- const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
5256
- attempt++;
5257
- clearRetry();
5258
- retryTimer = setTimeout(() => {
5259
- retryTimer = void 0;
5260
- void drain();
5261
- }, delay);
5262
- unrefTimer(retryTimer);
5263
- };
5264
- const drain = async () => {
5265
- try {
5266
- await loadPromise;
5267
- } catch {
5268
- }
5269
- if (flushing) return;
5270
- flushing = true;
5271
- try {
5272
- while (pending.length > 0) {
5273
- const batch = pending.slice(0, batchSize);
5274
- const ok = await postBatch(batch.map((item) => item.event));
5275
- if (!ok) {
5276
- scheduleRetry();
5277
- return;
5278
- }
5279
- const acked = new Set(batch.map((item) => item.id));
5280
- pending = pending.filter((item) => !acked.has(item.id));
5281
- persist();
5282
- attempt = 0;
5283
- clearRetry();
5284
- }
5285
- } finally {
5286
- flushing = false;
5287
- }
5288
- };
5289
- const flush = () => {
5290
- try {
5291
- void drain();
5292
- } catch {
5293
- }
5294
- };
5295
- const enqueue = (event) => {
5296
- try {
5297
- const stamped = stamp(event);
5298
- const sig = safeSig(stamped);
5299
- for (const item of pending) {
5300
- if (item.sig === sig) return;
5301
- }
5302
- pending.push({ id: nextId++, event: stamped, sig });
5303
- enforceSizeCap();
5304
- persist();
5305
- if (retryTimer === void 0) flush();
5306
- } catch {
5307
- }
5308
- };
5309
- const notifyOnline = () => {
5310
- attempt = 0;
5311
- clearRetry();
5312
- flush();
5313
- };
5314
- const size = () => pending.length;
5315
- return { enqueue, flush, notifyOnline, size };
5316
5397
  };
5317
-
5318
- // src/session-analytics/useLifecycleEvents.ts
5398
+ var wireLifecycleEvents = (opts) => {
5399
+ var _a;
5400
+ const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5401
+ reportFirstOpen({ ...opts, sessionId });
5402
+ reportSessionStart({
5403
+ target: opts.target,
5404
+ sink: opts.sink,
5405
+ sessionId,
5406
+ userId: opts.userId,
5407
+ deviceKey: opts.deviceKey,
5408
+ sessionCount: opts.sessionCount,
5409
+ appVersion: opts.appVersion,
5410
+ platform: opts.platform,
5411
+ device: opts.device,
5412
+ meta: opts.meta
5413
+ });
5414
+ };
5319
5415
  var useLifecycleEvents = (config, options = {}) => {
5320
5416
  const latest = useRef({ config, options });
5321
5417
  latest.current = { config, options };
5322
5418
  const queueRef = useRef(void 0);
5323
5419
  useEffect(() => {
5420
+ let cancelled = false;
5324
5421
  const resolveSink = () => {
5325
5422
  var _a, _b;
5326
5423
  const { config: cfg, options: opts } = latest.current;
@@ -5341,7 +5438,7 @@ var useLifecycleEvents = (config, options = {}) => {
5341
5438
  var _a;
5342
5439
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a = cfg.apiKey) != null ? _a : "" } : void 0;
5343
5440
  };
5344
- const resolveDeviceKey = (cfg, opts) => {
5441
+ const resolveDeviceKey = (cfg, opts, autoDeviceKey) => {
5345
5442
  var _a;
5346
5443
  const host = (_a = resolveIdentity({
5347
5444
  value: opts.deviceKey,
@@ -5350,11 +5447,22 @@ var useLifecycleEvents = (config, options = {}) => {
5350
5447
  scope: cfg == null ? void 0 : cfg.appId
5351
5448
  })) == null ? void 0 : _a.value;
5352
5449
  if (host) return host;
5353
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5354
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5450
+ return autoDeviceKey;
5451
+ };
5452
+ const openAutoDeviceKey = (fire) => {
5453
+ const { config: cfg, options: opts } = latest.current;
5454
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5455
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5456
+ fire(void 0);
5457
+ return;
5458
+ }
5459
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5460
+ if (cancelled) return;
5461
+ fire((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5462
+ });
5355
5463
  };
5356
5464
  const mountOpenSessionId = makeSessionId();
5357
- const fireSession = (sessionId) => {
5465
+ const fireSession = (sessionId, autoDeviceKey) => {
5358
5466
  var _a;
5359
5467
  const { config: cfg, options: opts } = latest.current;
5360
5468
  if (opts.enabled === false) return;
@@ -5366,7 +5474,7 @@ var useLifecycleEvents = (config, options = {}) => {
5366
5474
  sink: resolveSink(),
5367
5475
  sessionId,
5368
5476
  userId: opts.userId,
5369
- deviceKey: resolveDeviceKey(cfg, opts),
5477
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5370
5478
  sessionCount: opts.sessionCount,
5371
5479
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5372
5480
  platform: Platform.OS,
@@ -5374,9 +5482,9 @@ var useLifecycleEvents = (config, options = {}) => {
5374
5482
  meta: opts.meta
5375
5483
  });
5376
5484
  };
5377
- const fireMountOpen = () => {
5485
+ const fireMountOpen = (autoDeviceKey) => {
5378
5486
  var _a;
5379
- fireSession(mountOpenSessionId);
5487
+ fireSession(mountOpenSessionId, autoDeviceKey);
5380
5488
  const { config: cfg, options: opts } = latest.current;
5381
5489
  if (opts.enabled === false) return;
5382
5490
  const device = collectDeviceContext();
@@ -5388,7 +5496,7 @@ var useLifecycleEvents = (config, options = {}) => {
5388
5496
  storage: cfg == null ? void 0 : cfg.storage,
5389
5497
  appId: cfg == null ? void 0 : cfg.appId,
5390
5498
  userId: opts.userId,
5391
- deviceKey: resolveDeviceKey(cfg, opts),
5499
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5392
5500
  sessionCount: opts.sessionCount,
5393
5501
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5394
5502
  platform: Platform.OS,
@@ -5396,16 +5504,7 @@ var useLifecycleEvents = (config, options = {}) => {
5396
5504
  meta: opts.meta
5397
5505
  });
5398
5506
  };
5399
- let cancelled = false;
5400
- const { config: mountCfg, options: mountOpts } = latest.current;
5401
- const hostKey = typeof mountOpts.deviceKey === "string" && mountOpts.deviceKey.trim() ? mountOpts.deviceKey : void 0;
5402
- if (!hostKey && (mountCfg == null ? void 0 : mountCfg.storage)) {
5403
- void hydrateAutoDeviceKey({ appId: mountCfg.appId, storage: mountCfg.storage }).then(() => {
5404
- if (!cancelled) fireMountOpen();
5405
- });
5406
- } else {
5407
- fireMountOpen();
5408
- }
5507
+ openAutoDeviceKey(fireMountOpen);
5409
5508
  let backgroundedAt = null;
5410
5509
  const onChange = (state) => {
5411
5510
  if (state === "background" || state === "inactive") {
@@ -5415,13 +5514,18 @@ var useLifecycleEvents = (config, options = {}) => {
5415
5514
  if (state === "active") {
5416
5515
  const since = backgroundedAt;
5417
5516
  backgroundedAt = null;
5418
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fireSession();
5517
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) {
5518
+ openAutoDeviceKey((autoDeviceKey) => fireSession(void 0, autoDeviceKey));
5519
+ }
5419
5520
  }
5420
5521
  };
5421
5522
  const sub = AppState.addEventListener("change", onChange);
5422
5523
  return () => {
5524
+ var _a;
5423
5525
  cancelled = true;
5424
5526
  if (sub && typeof sub.remove === "function") sub.remove();
5527
+ (_a = queueRef.current) == null ? void 0 : _a.dispose();
5528
+ queueRef.current = void 0;
5425
5529
  };
5426
5530
  }, []);
5427
5531
  };