@wireai/activation 0.13.6 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +21 -9
  2. package/CHANGELOG.md +172 -1
  3. package/INTEGRATION_PROMPT.md +7 -4
  4. package/README.md +10 -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 +24 -6
  14. package/dist/index.d.ts +24 -6
  15. package/dist/index.js +631 -548
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +631 -548
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/questionnaire/index.js.map +1 -1
  20. package/dist/questionnaire/index.mjs.map +1 -1
  21. package/dist/reviews/index.js.map +1 -1
  22. package/dist/reviews/index.mjs.map +1 -1
  23. package/llms.txt +2 -2
  24. package/package.json +1 -1
  25. package/src/OnboardingFlow.tsx +4 -3
  26. package/src/WireOnboarding.tsx +4 -3
  27. package/src/activation/useWireActivation.ts +14 -1
  28. package/src/activation/wireActivation.ts +68 -2
  29. package/src/analytics/analyticsFacade.ts +24 -0
  30. package/src/analytics/eventQueue.ts +106 -11
  31. package/src/analytics/reportClientEvent.ts +31 -7
  32. package/src/analytics/useAnalytics.ts +17 -0
  33. package/src/context/deviceId.ts +10 -3
  34. package/src/permissions/permissionMemory.ts +12 -1
  35. package/src/session/persistedSession.ts +32 -8
  36. package/src/session-analytics/lifecycle.ts +26 -5
  37. package/src/session-analytics/reportSessionStart.ts +14 -10
  38. package/src/session-analytics/useLifecycleEvents.ts +70 -32
  39. package/src/session-analytics/useSessionStart.ts +57 -15
  40. package/src/types.ts +16 -6
  41. 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
  }
@@ -3803,7 +3809,9 @@ var readSettledPermissions = (raw, sessionId) => {
3803
3809
  };
3804
3810
  var loadSettledPermissions = 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 [];
3814
+ return readSettledPermissions(raw, sessionId);
3807
3815
  } catch {
3808
3816
  return [];
3809
3817
  }
@@ -3912,7 +3920,7 @@ var WireOnboarding = ({
3912
3920
  useEffect(() => {
3913
3921
  if (!warnMissingJoinKey) return;
3914
3922
  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
3923
+ "[wireai] <WireOnboarding> got no user_context.device_key, so this onboarding session can never be joined to the app's later events and the `activated` funnel will read zero. Pass userContext={activationJoinContext(deviceKey)} if your app owns a device id. If it does NOT, do not build one here \u2014 leave userContext alone and pass a working `storage` instead: the kit injects the same id the analytics side stamps, and only when it has confirmed the id actually persists. Never hand-write userContext={{ deviceKey }}. The kit did NOT auto-inject its own key here because " + autoJoinReason
3916
3924
  );
3917
3925
  }, [warnMissingJoinKey, autoJoinReason]);
3918
3926
  const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
@@ -4591,205 +4599,518 @@ var attributionMetadata = (a) => {
4591
4599
  return { attribution };
4592
4600
  };
4593
4601
 
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];
4602
+ // src/analytics/eventQueue.ts
4603
+ var DEFAULTS = {
4604
+ maxSize: 200,
4605
+ batchSize: 20,
4606
+ baseBackoffMs: 1e3,
4607
+ maxBackoffMs: 3e4,
4608
+ maxRetries: 6
4609
+ };
4610
+ var READ_TIMEOUT_MS3 = 1500;
4611
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
4612
+ var queueKeyGlobal = globalThis;
4613
+ var claimedQueueKeys = () => {
4614
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
4601
4615
  if (existing) return existing;
4602
- const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4603
- globalSlot2[REVALIDATION_SLOT] = created;
4616
+ const created = /* @__PURE__ */ new Set();
4617
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
4604
4618
  return created;
4605
4619
  };
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
- }
4620
+ var claimQueueKey = (preferred, explicit) => {
4621
+ const claimed = claimedQueueKeys();
4622
+ if (explicit || !claimed.has(preferred)) {
4623
+ claimed.add(preferred);
4624
+ return preferred;
4614
4625
  }
4626
+ let ordinal = 2;
4627
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
4628
+ const key = `${preferred}#${ordinal}`;
4629
+ claimed.add(key);
4630
+ warnInDev(
4631
+ `[wireai] a second event queue was created for the same appId, and "${preferred}" is already claimed by a live one. Two queues sharing one storage slot overwrite each other's backlog, delete each other's pending events when one drains to empty, and double-send on relaunch, so this queue was given "${key}" instead. Prefer ONE analytics instance per app; if you really need two, pass an explicit \`storageKey\` to each so the slots are yours to reason about.`
4632
+ );
4633
+ return key;
4615
4634
  };
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();
4635
+ var releaseQueueKey = (key) => {
4636
+ claimedQueueKeys().delete(key);
4628
4637
  };
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
4638
+ var READ_TIMED_OUT2 = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
4639
+ var withTimeout3 = (p, ms) => {
4640
+ let timer;
4641
+ const timeout = new Promise((resolve) => {
4642
+ timer = setTimeout(() => resolve(READ_TIMED_OUT2), ms);
4641
4643
  });
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;
4644
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4733
4645
  };
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";
4646
+ var unrefTimer = (timer) => {
4647
+ const t = timer;
4648
+ if (typeof t.unref === "function") t.unref();
4739
4649
  };
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;
4650
+ var parsePersisted2 = (raw) => {
4651
+ if (!raw) return [];
4652
+ try {
4653
+ const parsed = JSON.parse(raw);
4654
+ if (!Array.isArray(parsed)) return [];
4655
+ const items = [];
4656
+ for (const entry of parsed) {
4657
+ if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
4658
+ items.push(entry);
4659
+ }
4751
4660
  }
4752
- if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
4753
- }
4754
- return props;
4755
- };
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"
4761
- };
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;
4661
+ return items;
4662
+ } catch {
4663
+ return [];
4769
4664
  }
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;
4778
- return record.code === PURCHASE_CANCELLED_CODE;
4779
- };
4780
- var describeFailure = (error) => {
4781
- const record = asRecord(error);
4782
- const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
4783
- return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
4784
4665
  };
4785
-
4786
- // src/revenuecat/revenueCatBridge.ts
4787
- var createRevenueCatBridge = (config) => {
4788
- const { analytics, entitlementId } = config;
4789
- const writeTier = config.writePlanTier !== false;
4790
- const emit = (event, props) => {
4666
+ var createEventQueue = (options) => {
4667
+ var _a, _b, _c, _d, _e, _f, _g;
4668
+ const target = options.target;
4669
+ const storage = options.storage;
4670
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
4671
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4672
+ const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4673
+ const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4674
+ const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
4675
+ const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
4676
+ const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
4677
+ let pending = [];
4678
+ let nextId = 0;
4679
+ let flushing = false;
4680
+ let attempt = 0;
4681
+ let retryTimer;
4682
+ let disposed = false;
4683
+ let backlogUnread = storage !== void 0;
4684
+ const resolveEnvelope = () => {
4791
4685
  try {
4792
- const result = analytics.track(event, { ...config.commonProps, ...props });
4686
+ return typeof options.envelope === "function" ? options.envelope() : options.envelope;
4687
+ } catch {
4688
+ return void 0;
4689
+ }
4690
+ };
4691
+ const stamp = (event) => {
4692
+ var _a2;
4693
+ const env = resolveEnvelope();
4694
+ const stamped = { ...event };
4695
+ if (stamped.ts === void 0) stamped.ts = Date.now();
4696
+ if (!env) return stamped;
4697
+ if (!stamped.device && env.device) stamped.device = env.device;
4698
+ if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
4699
+ const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
4700
+ if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
4701
+ if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
4702
+ if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
4703
+ if (Object.keys(uc).length > 0) stamped.user_context = uc;
4704
+ return stamped;
4705
+ };
4706
+ const persist = () => {
4707
+ if (!storage) return;
4708
+ if (disposed) return;
4709
+ if (backlogUnread) return;
4710
+ try {
4711
+ if (pending.length === 0) {
4712
+ void storage.removeItem(key).catch(() => {
4713
+ });
4714
+ return;
4715
+ }
4716
+ const payload = pending.map((item) => ({ id: item.id, event: item.event }));
4717
+ void storage.setItem(key, JSON.stringify(payload)).catch(() => {
4718
+ });
4719
+ } catch {
4720
+ }
4721
+ };
4722
+ const isCountedOpenEvent = (event) => typeof event.question_key === "string" && event.question_key.startsWith("app.");
4723
+ const enforceSizeCap = () => {
4724
+ let overflow = pending.length - maxSize;
4725
+ if (overflow <= 0) return;
4726
+ const kept = [];
4727
+ for (const item of pending) {
4728
+ if (overflow > 0 && !isCountedOpenEvent(item.event)) {
4729
+ overflow--;
4730
+ continue;
4731
+ }
4732
+ kept.push(item);
4733
+ }
4734
+ if (overflow > 0) kept.splice(0, overflow);
4735
+ pending = kept;
4736
+ };
4737
+ const safeSig = (event) => {
4738
+ try {
4739
+ return JSON.stringify(event);
4740
+ } catch {
4741
+ return `__nosig_${nextId}_${Math.random()}`;
4742
+ }
4743
+ };
4744
+ const mergePersisted = (persistedItems) => {
4745
+ if (persistedItems.length === 0) return;
4746
+ const seen = new Set(pending.map((item) => item.sig));
4747
+ const restored = [];
4748
+ for (const persisted of persistedItems) {
4749
+ const sig = safeSig(persisted.event);
4750
+ if (seen.has(sig)) continue;
4751
+ seen.add(sig);
4752
+ restored.push({ id: nextId++, event: persisted.event, sig });
4753
+ }
4754
+ if (restored.length === 0) return;
4755
+ pending = [...restored, ...pending];
4756
+ enforceSizeCap();
4757
+ persist();
4758
+ };
4759
+ const loadPromise = (async () => {
4760
+ if (!storage) return;
4761
+ try {
4762
+ const read = Promise.resolve(storage.getItem(key));
4763
+ read.catch(() => {
4764
+ });
4765
+ const raced = await withTimeout3(read, READ_TIMEOUT_MS3);
4766
+ if (raced === READ_TIMED_OUT2) {
4767
+ backlogUnread = true;
4768
+ warnInDev(
4769
+ `[wireai] the persisted analytics backlog at "${key}" took longer than ${READ_TIMEOUT_MS3}ms to read, so the queue started without it. The stored events are NOT discarded: writes are held back until the read lands, and the backlog is merged in then. If you see this on every cold start, your storage adapter is too slow to be on the launch path.`
4770
+ );
4771
+ void read.then((late) => {
4772
+ backlogUnread = false;
4773
+ mergePersisted(parsePersisted2(late));
4774
+ flush();
4775
+ }).catch(() => {
4776
+ backlogUnread = false;
4777
+ persist();
4778
+ });
4779
+ return;
4780
+ }
4781
+ backlogUnread = false;
4782
+ mergePersisted(parsePersisted2(raced));
4783
+ persist();
4784
+ } catch {
4785
+ backlogUnread = false;
4786
+ persist();
4787
+ }
4788
+ })();
4789
+ const postBatch = async (events) => {
4790
+ const req = buildEventsRequest(target, events);
4791
+ if (!req) return false;
4792
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
4793
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
4794
+ try {
4795
+ const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
4796
+ warnOnSkippedEvents(res);
4797
+ return !!(res && res.ok);
4798
+ } catch {
4799
+ return false;
4800
+ } finally {
4801
+ clearTimeout(timer);
4802
+ }
4803
+ };
4804
+ const clearRetry = () => {
4805
+ if (retryTimer !== void 0) {
4806
+ clearTimeout(retryTimer);
4807
+ retryTimer = void 0;
4808
+ }
4809
+ };
4810
+ const scheduleRetry = () => {
4811
+ if (attempt >= maxRetries) return;
4812
+ const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
4813
+ attempt++;
4814
+ clearRetry();
4815
+ retryTimer = setTimeout(() => {
4816
+ retryTimer = void 0;
4817
+ void drain();
4818
+ }, delay);
4819
+ unrefTimer(retryTimer);
4820
+ };
4821
+ const drain = async () => {
4822
+ if (disposed) return;
4823
+ try {
4824
+ await loadPromise;
4825
+ } catch {
4826
+ }
4827
+ if (disposed) return;
4828
+ if (flushing) return;
4829
+ flushing = true;
4830
+ try {
4831
+ while (pending.length > 0) {
4832
+ const batch = pending.slice(0, batchSize);
4833
+ const ok = await postBatch(batch.map((item) => item.event));
4834
+ if (!ok) {
4835
+ scheduleRetry();
4836
+ return;
4837
+ }
4838
+ const acked = new Set(batch.map((item) => item.id));
4839
+ pending = pending.filter((item) => !acked.has(item.id));
4840
+ persist();
4841
+ attempt = 0;
4842
+ clearRetry();
4843
+ }
4844
+ } finally {
4845
+ flushing = false;
4846
+ }
4847
+ };
4848
+ const flush = () => {
4849
+ try {
4850
+ void drain();
4851
+ } catch {
4852
+ }
4853
+ };
4854
+ const enqueue = (event) => {
4855
+ if (disposed) return;
4856
+ try {
4857
+ const stamped = stamp(event);
4858
+ const sig = safeSig(stamped);
4859
+ for (const item of pending) {
4860
+ if (item.sig === sig) return;
4861
+ }
4862
+ pending.push({ id: nextId++, event: stamped, sig });
4863
+ enforceSizeCap();
4864
+ persist();
4865
+ if (retryTimer === void 0) flush();
4866
+ } catch {
4867
+ }
4868
+ };
4869
+ const notifyOnline = () => {
4870
+ attempt = 0;
4871
+ clearRetry();
4872
+ flush();
4873
+ };
4874
+ const size = () => pending.length;
4875
+ const dispose = () => {
4876
+ if (disposed) return;
4877
+ disposed = true;
4878
+ clearRetry();
4879
+ if (storage) releaseQueueKey(key);
4880
+ };
4881
+ return { enqueue, flush, notifyOnline, size, dispose };
4882
+ };
4883
+
4884
+ // src/activation/revalidation.ts
4885
+ var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4886
+ "@wireai/activation:activationRevalidation"
4887
+ );
4888
+ var globalSlot2 = globalThis;
4889
+ var store = () => {
4890
+ const existing = globalSlot2[REVALIDATION_SLOT];
4891
+ if (existing) return existing;
4892
+ const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4893
+ globalSlot2[REVALIDATION_SLOT] = created;
4894
+ return created;
4895
+ };
4896
+ var bumpActivationRevalidation = () => {
4897
+ const s = store();
4898
+ s.version += 1;
4899
+ for (const listener of Array.from(s.listeners)) {
4900
+ try {
4901
+ listener();
4902
+ } catch {
4903
+ }
4904
+ }
4905
+ };
4906
+ var subscribeActivationRevalidation = (listener) => {
4907
+ const s = store();
4908
+ s.listeners.add(listener);
4909
+ return () => {
4910
+ s.listeners.delete(listener);
4911
+ };
4912
+ };
4913
+ var getActivationRevalidationVersion = () => store().version;
4914
+ var resetActivationRevalidation = () => {
4915
+ const s = store();
4916
+ s.version = 0;
4917
+ s.listeners.clear();
4918
+ };
4919
+
4920
+ // src/activation/wireActivation.ts
4921
+ var clean = cleanString;
4922
+ var createWireActivation = (config) => {
4923
+ var _a, _b;
4924
+ const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4925
+ const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4926
+ resolveIdentity({
4927
+ value: explicitDeviceKey,
4928
+ space: "device",
4929
+ source: "host",
4930
+ scope: config.appId
4931
+ });
4932
+ const autoDeviceKeyOptions = {
4933
+ appId: config.appId,
4934
+ // An explicit key opts out of minting AND persisting (unchanged contract).
4935
+ storage: explicitDeviceKey ? void 0 : config.storage
4936
+ };
4937
+ if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4938
+ const detectedAppVersion = detectAppVersion();
4939
+ const applyContext = (event) => {
4940
+ var _a2, _b2, _c;
4941
+ const resolved = resolveUserContext(
4942
+ // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4943
+ {
4944
+ ...(_a2 = config.userContext) != null ? _a2 : {},
4945
+ deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4946
+ },
4947
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4948
+ );
4949
+ if (resolved.userContext) {
4950
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4951
+ }
4952
+ if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4953
+ };
4954
+ let durability;
4955
+ const bufferFailedEvent = (event) => {
4956
+ var _a2;
4957
+ if (config.sink) {
4958
+ config.sink(event);
4959
+ return;
4960
+ }
4961
+ if (!durability) {
4962
+ durability = createEventQueue({
4963
+ target,
4964
+ storage: config.storage,
4965
+ storageKey: `wireai:evtq:activation:${(_a2 = config.appId) != null ? _a2 : "default"}`
4966
+ });
4967
+ }
4968
+ durability.enqueue(event);
4969
+ };
4970
+ const track = async (name, meta) => {
4971
+ if (!clean(name)) return false;
4972
+ const sessionId = ensureCurrentSessionId();
4973
+ const event = {
4974
+ event_type: "app_event",
4975
+ session_id: sessionId,
4976
+ question_key: name
4977
+ };
4978
+ if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
4979
+ applyContext(event);
4980
+ const outcome = await reportClientEventsOutcome(target, [event]);
4981
+ const ok = outcome === "delivered";
4982
+ if (ok) bumpActivationRevalidation();
4983
+ if (outcome === "unreachable") bufferFailedEvent(event);
4984
+ return ok;
4985
+ };
4986
+ return {
4987
+ track,
4988
+ get sessionId() {
4989
+ return getCurrentSessionId();
4990
+ },
4991
+ subscribeRevalidation: subscribeActivationRevalidation,
4992
+ getRevalidationVersion: getActivationRevalidationVersion,
4993
+ dispose: () => {
4994
+ durability == null ? void 0 : durability.dispose();
4995
+ durability = void 0;
4996
+ }
4997
+ };
4998
+ };
4999
+ var useActivationRevalidation = () => useSyncExternalStore(
5000
+ subscribeActivationRevalidation,
5001
+ getActivationRevalidationVersion,
5002
+ getActivationRevalidationVersion
5003
+ );
5004
+ var useWireActivation = (config) => {
5005
+ var _a, _b;
5006
+ const ref = useRef(void 0);
5007
+ const prevKeys = useRef("");
5008
+ const currentKeys = [
5009
+ config.serverUrl,
5010
+ config.apiKey,
5011
+ config.appId,
5012
+ config.deviceKey,
5013
+ (_a = config.userContext) == null ? void 0 : _a.deviceKey
5014
+ ].join("|");
5015
+ if (!ref.current || prevKeys.current !== currentKeys) {
5016
+ prevKeys.current = currentKeys;
5017
+ (_b = ref.current) == null ? void 0 : _b.dispose();
5018
+ ref.current = createWireActivation(config);
5019
+ }
5020
+ useEffect(
5021
+ () => () => {
5022
+ var _a2;
5023
+ (_a2 = ref.current) == null ? void 0 : _a2.dispose();
5024
+ ref.current = void 0;
5025
+ },
5026
+ []
5027
+ );
5028
+ const revalidation = useActivationRevalidation();
5029
+ return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
5030
+ };
5031
+
5032
+ // src/revenuecat/purchaseEvents.ts
5033
+ var WIRE_PURCHASE_EVENTS = {
5034
+ /** The paywall became visible (offerings loaded). */
5035
+ paywallShown: "wire_paywall_shown",
5036
+ /** The user tapped buy; the store sheet is about to open. */
5037
+ checkoutStarted: "wire_checkout_started",
5038
+ /** The store confirmed the purchase AND the entitlement is now active. */
5039
+ purchased: "wire_purchase_completed",
5040
+ /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
5041
+ purchaseFailed: "wire_purchase_failed",
5042
+ /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
5043
+ restored: "wire_purchase_restored"
5044
+ };
5045
+ var PLAN_TIER_CONTEXT_KEY = "plan_tier";
5046
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
5047
+ var activeEntitlement = (info, entitlementId) => {
5048
+ var _a, _b;
5049
+ const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
5050
+ const found = active == null ? void 0 : active[entitlementId];
5051
+ const entitlement = asRecord(found);
5052
+ if (!entitlement) return void 0;
5053
+ return entitlement.isActive === false ? void 0 : entitlement;
5054
+ };
5055
+ var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
5056
+ var resolvePlanTier = (info, entitlementId) => {
5057
+ const entitlement = activeEntitlement(info, entitlementId);
5058
+ if (!entitlement) return "free";
5059
+ return isTrial(entitlement) ? "trial" : "paid";
5060
+ };
5061
+ var describePackage = (pkg) => {
5062
+ const props = {};
5063
+ if (!pkg) return props;
5064
+ if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
5065
+ if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
5066
+ if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
5067
+ const product = asRecord(pkg.product);
5068
+ if (product) {
5069
+ if (typeof product.identifier === "string") props.product_id = product.identifier;
5070
+ if (typeof product.price === "number" && Number.isFinite(product.price)) {
5071
+ props.price = product.price;
5072
+ }
5073
+ if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
5074
+ }
5075
+ return props;
5076
+ };
5077
+ var describeEntitlement = (info, entitlementId) => {
5078
+ const entitlement = activeEntitlement(info, entitlementId);
5079
+ const props = {
5080
+ entitlement: entitlementId,
5081
+ plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
5082
+ };
5083
+ if (!entitlement) return props;
5084
+ if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
5085
+ props.is_trial = isTrial(entitlement);
5086
+ if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
5087
+ if (typeof entitlement.store === "string") props.store = entitlement.store;
5088
+ if (typeof entitlement.productIdentifier === "string") {
5089
+ props.product_id = entitlement.productIdentifier;
5090
+ }
5091
+ if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
5092
+ return props;
5093
+ };
5094
+ var PURCHASE_CANCELLED_CODE = "1";
5095
+ var isUserCancelled = (error) => {
5096
+ const record = asRecord(error);
5097
+ if (!record) return false;
5098
+ if (record.userCancelled === true) return true;
5099
+ return record.code === PURCHASE_CANCELLED_CODE;
5100
+ };
5101
+ var describeFailure = (error) => {
5102
+ const record = asRecord(error);
5103
+ const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
5104
+ return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
5105
+ };
5106
+
5107
+ // src/revenuecat/revenueCatBridge.ts
5108
+ var createRevenueCatBridge = (config) => {
5109
+ const { analytics, entitlementId } = config;
5110
+ const writeTier = config.writePlanTier !== false;
5111
+ const emit = (event, props) => {
5112
+ try {
5113
+ const result = analytics.track(event, { ...config.commonProps, ...props });
4793
5114
  if (result && typeof result.catch === "function") {
4794
5115
  void result.catch(() => {
4795
5116
  });
@@ -4900,15 +5221,9 @@ var reportSessionStart = (opts) => {
4900
5221
  opts.sink(event);
4901
5222
  return;
4902
5223
  }
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) => {
5224
+ const req = buildEventsRequest(target, [event]);
5225
+ if (!req) return;
5226
+ void fetch(req.url, req.init).then((res) => {
4912
5227
  warnOnSkippedEvents(res);
4913
5228
  }).catch(() => {
4914
5229
  });
@@ -4920,13 +5235,25 @@ var useSessionStart = (config, options = {}) => {
4920
5235
  const latest = useRef({ config, options });
4921
5236
  latest.current = { config, options };
4922
5237
  useEffect(() => {
4923
- const resolveDeviceKey = (cfg, opts) => {
5238
+ let cancelled = false;
5239
+ const resolveDeviceKey = (opts, autoDeviceKey) => {
4924
5240
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4925
5241
  if (host) return host;
4926
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4927
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5242
+ return autoDeviceKey;
5243
+ };
5244
+ const openAutoDeviceKey = (fireOpen) => {
5245
+ const { config: cfg, options: opts } = latest.current;
5246
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5247
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5248
+ fireOpen(void 0);
5249
+ return;
5250
+ }
5251
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5252
+ if (cancelled) return;
5253
+ fireOpen((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5254
+ });
4928
5255
  };
4929
- const fire = () => {
5256
+ const fire = (autoDeviceKey) => {
4930
5257
  var _a, _b;
4931
5258
  const { config: cfg, options: opts } = latest.current;
4932
5259
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return;
@@ -4938,7 +5265,7 @@ var useSessionStart = (config, options = {}) => {
4938
5265
  target,
4939
5266
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4940
5267
  userId: opts.userId,
4941
- deviceKey: resolveDeviceKey(cfg, opts),
5268
+ deviceKey: resolveDeviceKey(opts, autoDeviceKey),
4942
5269
  sessionCount: opts.sessionCount,
4943
5270
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4944
5271
  platform: Platform.OS,
@@ -4946,7 +5273,7 @@ var useSessionStart = (config, options = {}) => {
4946
5273
  meta: opts.meta
4947
5274
  });
4948
5275
  };
4949
- fire();
5276
+ openAutoDeviceKey(fire);
4950
5277
  let backgroundedAt = null;
4951
5278
  const onChange = (state) => {
4952
5279
  if (state === "background" || state === "inactive") {
@@ -4956,11 +5283,12 @@ var useSessionStart = (config, options = {}) => {
4956
5283
  if (state === "active") {
4957
5284
  const since = backgroundedAt;
4958
5285
  backgroundedAt = null;
4959
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fire();
5286
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) openAutoDeviceKey(fire);
4960
5287
  }
4961
5288
  };
4962
5289
  const sub = AppState.addEventListener("change", onChange);
4963
5290
  return () => {
5291
+ cancelled = true;
4964
5292
  if (sub && typeof sub.remove === "function") sub.remove();
4965
5293
  };
4966
5294
  }, []);
@@ -4969,11 +5297,12 @@ var useSessionStart = (config, options = {}) => {
4969
5297
  // src/session-analytics/lifecycle.ts
4970
5298
  var FIRST_OPEN_EVENT = "app.first_open";
4971
5299
  var firstOpenStorageKey = (appId) => `wireai:first_open:${appId}`;
4972
- var READ_TIMEOUT_MS3 = 1500;
4973
- var withTimeout3 = (p, ms) => {
5300
+ var READ_TIMEOUT_MS4 = 1500;
5301
+ var READ_TIMED_OUT3 = /* @__PURE__ */ Symbol("wireai:first-open-read-timeout");
5302
+ var withTimeout4 = (p, ms) => {
4974
5303
  let timer;
4975
5304
  const timeout = new Promise((resolve) => {
4976
- timer = setTimeout(() => resolve(void 0), ms);
5305
+ timer = setTimeout(() => resolve(READ_TIMED_OUT3), ms);
4977
5306
  });
4978
5307
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4979
5308
  };
@@ -4992,335 +5321,82 @@ var buildLifecycleEvent = (questionKey, opts) => {
4992
5321
  if (opts.appVersion) userContext.app_version = opts.appVersion;
4993
5322
  if (opts.platform) userContext.platform = opts.platform;
4994
5323
  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();
5324
+ event_type: "app_event",
5325
+ question_key: questionKey,
5326
+ session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
5327
+ };
5328
+ const userId = sanitizeUserId(opts.userId);
5329
+ if (userId) event.user_id = userId;
5330
+ if (Object.keys(userContext).length > 0) event.user_context = userContext;
5331
+ if (opts.device) event.device = opts.device;
5332
+ if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
5333
+ return event;
5110
5334
  };
5111
- var parsePersisted2 = (raw) => {
5112
- if (!raw) return [];
5335
+ var routeLifecycleEvent = (event, opts) => {
5113
5336
  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
- }
5337
+ if (opts.sink) {
5338
+ opts.sink(event);
5339
+ return;
5121
5340
  }
5122
- return items;
5341
+ const req = buildEventsRequest(opts.target, [event]);
5342
+ if (!req) return;
5343
+ void fetch(req.url, req.init).catch(() => {
5344
+ });
5123
5345
  } catch {
5124
- return [];
5125
5346
  }
5126
5347
  };
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;
5348
+ var emitFirstOpen = (opts) => {
5349
+ routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5350
+ };
5351
+ var reportFirstOpen = (opts) => {
5352
+ var _a;
5353
+ const appId = (_a = opts.appId) != null ? _a : "default";
5354
+ if (_firstOpenLatched.has(appId)) return;
5355
+ _firstOpenLatched.add(appId);
5356
+ const storage = opts.storage;
5357
+ if (!storage) {
5358
+ emitFirstOpen(opts);
5359
+ return;
5360
+ }
5361
+ const key = firstOpenStorageKey(appId);
5362
+ void (async () => {
5208
5363
  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();
5364
+ const seen = await withTimeout4(storage.getItem(key), READ_TIMEOUT_MS4);
5365
+ if (seen === READ_TIMED_OUT3) return;
5366
+ if (seen) return;
5367
+ emitFirstOpen(opts);
5368
+ try {
5369
+ void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5225
5370
  });
5226
- return;
5371
+ } catch {
5227
5372
  }
5228
- mergePersisted(parsePersisted2(raced));
5229
5373
  } catch {
5230
5374
  }
5231
5375
  })();
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
5376
  };
5317
-
5318
- // src/session-analytics/useLifecycleEvents.ts
5377
+ var wireLifecycleEvents = (opts) => {
5378
+ var _a;
5379
+ const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5380
+ reportFirstOpen({ ...opts, sessionId });
5381
+ reportSessionStart({
5382
+ target: opts.target,
5383
+ sink: opts.sink,
5384
+ sessionId,
5385
+ userId: opts.userId,
5386
+ deviceKey: opts.deviceKey,
5387
+ sessionCount: opts.sessionCount,
5388
+ appVersion: opts.appVersion,
5389
+ platform: opts.platform,
5390
+ device: opts.device,
5391
+ meta: opts.meta
5392
+ });
5393
+ };
5319
5394
  var useLifecycleEvents = (config, options = {}) => {
5320
5395
  const latest = useRef({ config, options });
5321
5396
  latest.current = { config, options };
5322
5397
  const queueRef = useRef(void 0);
5323
5398
  useEffect(() => {
5399
+ let cancelled = false;
5324
5400
  const resolveSink = () => {
5325
5401
  var _a, _b;
5326
5402
  const { config: cfg, options: opts } = latest.current;
@@ -5341,7 +5417,7 @@ var useLifecycleEvents = (config, options = {}) => {
5341
5417
  var _a;
5342
5418
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a = cfg.apiKey) != null ? _a : "" } : void 0;
5343
5419
  };
5344
- const resolveDeviceKey = (cfg, opts) => {
5420
+ const resolveDeviceKey = (cfg, opts, autoDeviceKey) => {
5345
5421
  var _a;
5346
5422
  const host = (_a = resolveIdentity({
5347
5423
  value: opts.deviceKey,
@@ -5350,11 +5426,22 @@ var useLifecycleEvents = (config, options = {}) => {
5350
5426
  scope: cfg == null ? void 0 : cfg.appId
5351
5427
  })) == null ? void 0 : _a.value;
5352
5428
  if (host) return host;
5353
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5354
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5429
+ return autoDeviceKey;
5430
+ };
5431
+ const openAutoDeviceKey = (fire) => {
5432
+ const { config: cfg, options: opts } = latest.current;
5433
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5434
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5435
+ fire(void 0);
5436
+ return;
5437
+ }
5438
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5439
+ if (cancelled) return;
5440
+ fire((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5441
+ });
5355
5442
  };
5356
5443
  const mountOpenSessionId = makeSessionId();
5357
- const fireSession = (sessionId) => {
5444
+ const fireSession = (sessionId, autoDeviceKey) => {
5358
5445
  var _a;
5359
5446
  const { config: cfg, options: opts } = latest.current;
5360
5447
  if (opts.enabled === false) return;
@@ -5366,7 +5453,7 @@ var useLifecycleEvents = (config, options = {}) => {
5366
5453
  sink: resolveSink(),
5367
5454
  sessionId,
5368
5455
  userId: opts.userId,
5369
- deviceKey: resolveDeviceKey(cfg, opts),
5456
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5370
5457
  sessionCount: opts.sessionCount,
5371
5458
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5372
5459
  platform: Platform.OS,
@@ -5374,9 +5461,9 @@ var useLifecycleEvents = (config, options = {}) => {
5374
5461
  meta: opts.meta
5375
5462
  });
5376
5463
  };
5377
- const fireMountOpen = () => {
5464
+ const fireMountOpen = (autoDeviceKey) => {
5378
5465
  var _a;
5379
- fireSession(mountOpenSessionId);
5466
+ fireSession(mountOpenSessionId, autoDeviceKey);
5380
5467
  const { config: cfg, options: opts } = latest.current;
5381
5468
  if (opts.enabled === false) return;
5382
5469
  const device = collectDeviceContext();
@@ -5388,7 +5475,7 @@ var useLifecycleEvents = (config, options = {}) => {
5388
5475
  storage: cfg == null ? void 0 : cfg.storage,
5389
5476
  appId: cfg == null ? void 0 : cfg.appId,
5390
5477
  userId: opts.userId,
5391
- deviceKey: resolveDeviceKey(cfg, opts),
5478
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5392
5479
  sessionCount: opts.sessionCount,
5393
5480
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5394
5481
  platform: Platform.OS,
@@ -5396,16 +5483,7 @@ var useLifecycleEvents = (config, options = {}) => {
5396
5483
  meta: opts.meta
5397
5484
  });
5398
5485
  };
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
- }
5486
+ openAutoDeviceKey(fireMountOpen);
5409
5487
  let backgroundedAt = null;
5410
5488
  const onChange = (state) => {
5411
5489
  if (state === "background" || state === "inactive") {
@@ -5415,13 +5493,18 @@ var useLifecycleEvents = (config, options = {}) => {
5415
5493
  if (state === "active") {
5416
5494
  const since = backgroundedAt;
5417
5495
  backgroundedAt = null;
5418
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fireSession();
5496
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) {
5497
+ openAutoDeviceKey((autoDeviceKey) => fireSession(void 0, autoDeviceKey));
5498
+ }
5419
5499
  }
5420
5500
  };
5421
5501
  const sub = AppState.addEventListener("change", onChange);
5422
5502
  return () => {
5503
+ var _a;
5423
5504
  cancelled = true;
5424
5505
  if (sub && typeof sub.remove === "function") sub.remove();
5506
+ (_a = queueRef.current) == null ? void 0 : _a.dispose();
5507
+ queueRef.current = void 0;
5425
5508
  };
5426
5509
  }, []);
5427
5510
  };