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