@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.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
  }
@@ -3809,7 +3815,9 @@ var readSettledPermissions = (raw, sessionId) => {
3809
3815
  };
3810
3816
  var loadSettledPermissions = 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 [];
3820
+ return readSettledPermissions(raw, sessionId);
3813
3821
  } catch {
3814
3822
  return [];
3815
3823
  }
@@ -3918,7 +3926,7 @@ var WireOnboarding = ({
3918
3926
  React19.useEffect(() => {
3919
3927
  if (!warnMissingJoinKey) return;
3920
3928
  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
3929
+ "[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
3930
  );
3923
3931
  }, [warnMissingJoinKey, autoJoinReason]);
3924
3932
  const hostKeyElsewhere = missingJoinKey ? hostIdentity("device", config.appId) : void 0;
@@ -4597,205 +4605,518 @@ var attributionMetadata = (a) => {
4597
4605
  return { attribution };
4598
4606
  };
4599
4607
 
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];
4608
+ // src/analytics/eventQueue.ts
4609
+ var DEFAULTS = {
4610
+ maxSize: 200,
4611
+ batchSize: 20,
4612
+ baseBackoffMs: 1e3,
4613
+ maxBackoffMs: 3e4,
4614
+ maxRetries: 6
4615
+ };
4616
+ var READ_TIMEOUT_MS3 = 1500;
4617
+ var QUEUE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:eventQueueKeys");
4618
+ var queueKeyGlobal = globalThis;
4619
+ var claimedQueueKeys = () => {
4620
+ const existing = queueKeyGlobal[QUEUE_KEY_SLOT];
4607
4621
  if (existing) return existing;
4608
- const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4609
- globalSlot2[REVALIDATION_SLOT] = created;
4622
+ const created = /* @__PURE__ */ new Set();
4623
+ queueKeyGlobal[QUEUE_KEY_SLOT] = created;
4610
4624
  return created;
4611
4625
  };
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
- }
4626
+ var claimQueueKey = (preferred, explicit) => {
4627
+ const claimed = claimedQueueKeys();
4628
+ if (explicit || !claimed.has(preferred)) {
4629
+ claimed.add(preferred);
4630
+ return preferred;
4620
4631
  }
4632
+ let ordinal = 2;
4633
+ while (claimed.has(`${preferred}#${ordinal}`)) ordinal++;
4634
+ const key = `${preferred}#${ordinal}`;
4635
+ claimed.add(key);
4636
+ warnInDev(
4637
+ `[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.`
4638
+ );
4639
+ return key;
4621
4640
  };
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();
4641
+ var releaseQueueKey = (key) => {
4642
+ claimedQueueKeys().delete(key);
4634
4643
  };
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
4644
+ var READ_TIMED_OUT2 = /* @__PURE__ */ Symbol("wireai:storage-read-timeout");
4645
+ var withTimeout3 = (p, ms) => {
4646
+ let timer;
4647
+ const timeout = new Promise((resolve) => {
4648
+ timer = setTimeout(() => resolve(READ_TIMED_OUT2), ms);
4647
4649
  });
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;
4650
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4739
4651
  };
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";
4652
+ var unrefTimer = (timer) => {
4653
+ const t = timer;
4654
+ if (typeof t.unref === "function") t.unref();
4745
4655
  };
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;
4656
+ var parsePersisted2 = (raw) => {
4657
+ if (!raw) return [];
4658
+ try {
4659
+ const parsed = JSON.parse(raw);
4660
+ if (!Array.isArray(parsed)) return [];
4661
+ const items = [];
4662
+ for (const entry of parsed) {
4663
+ if (entry && typeof entry === "object" && typeof entry.id === "number" && entry.event && typeof entry.event === "object") {
4664
+ items.push(entry);
4665
+ }
4757
4666
  }
4758
- if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
4759
- }
4760
- return props;
4761
- };
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"
4767
- };
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;
4667
+ return items;
4668
+ } catch {
4669
+ return [];
4775
4670
  }
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;
4784
- return record.code === PURCHASE_CANCELLED_CODE;
4785
- };
4786
- var describeFailure = (error) => {
4787
- const record = asRecord(error);
4788
- const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
4789
- return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
4790
4671
  };
4791
-
4792
- // src/revenuecat/revenueCatBridge.ts
4793
- var createRevenueCatBridge = (config) => {
4794
- const { analytics, entitlementId } = config;
4795
- const writeTier = config.writePlanTier !== false;
4796
- const emit = (event, props) => {
4672
+ var createEventQueue = (options) => {
4673
+ var _a, _b, _c, _d, _e, _f, _g;
4674
+ const target = options.target;
4675
+ const storage = options.storage;
4676
+ const defaultKey = (_b = options.storageKey) != null ? _b : `wireai:evtq:${(_a = options.appId) != null ? _a : "default"}`;
4677
+ const key = storage ? claimQueueKey(defaultKey, options.storageKey !== void 0) : defaultKey;
4678
+ const maxSize = (_c = options.maxSize) != null ? _c : DEFAULTS.maxSize;
4679
+ const batchSize = (_d = options.batchSize) != null ? _d : DEFAULTS.batchSize;
4680
+ const baseBackoffMs = (_e = options.baseBackoffMs) != null ? _e : DEFAULTS.baseBackoffMs;
4681
+ const maxBackoffMs = (_f = options.maxBackoffMs) != null ? _f : DEFAULTS.maxBackoffMs;
4682
+ const maxRetries = (_g = options.maxRetries) != null ? _g : DEFAULTS.maxRetries;
4683
+ let pending = [];
4684
+ let nextId = 0;
4685
+ let flushing = false;
4686
+ let attempt = 0;
4687
+ let retryTimer;
4688
+ let disposed = false;
4689
+ let backlogUnread = storage !== void 0;
4690
+ const resolveEnvelope = () => {
4797
4691
  try {
4798
- const result = analytics.track(event, { ...config.commonProps, ...props });
4692
+ return typeof options.envelope === "function" ? options.envelope() : options.envelope;
4693
+ } catch {
4694
+ return void 0;
4695
+ }
4696
+ };
4697
+ const stamp = (event) => {
4698
+ var _a2;
4699
+ const env = resolveEnvelope();
4700
+ const stamped = { ...event };
4701
+ if (stamped.ts === void 0) stamped.ts = Date.now();
4702
+ if (!env) return stamped;
4703
+ if (!stamped.device && env.device) stamped.device = env.device;
4704
+ if (!stamped.session_id && env.sessionId) stamped.session_id = env.sessionId;
4705
+ const uc = { ...(_a2 = stamped.user_context) != null ? _a2 : {} };
4706
+ if (env.appVersion && uc.app_version === void 0) uc.app_version = env.appVersion;
4707
+ if (env.appBuild && uc.app_build === void 0) uc.app_build = env.appBuild;
4708
+ if (env.networkType && uc.network_type === void 0) uc.network_type = env.networkType;
4709
+ if (Object.keys(uc).length > 0) stamped.user_context = uc;
4710
+ return stamped;
4711
+ };
4712
+ const persist = () => {
4713
+ if (!storage) return;
4714
+ if (disposed) return;
4715
+ if (backlogUnread) return;
4716
+ try {
4717
+ if (pending.length === 0) {
4718
+ void storage.removeItem(key).catch(() => {
4719
+ });
4720
+ return;
4721
+ }
4722
+ const payload = pending.map((item) => ({ id: item.id, event: item.event }));
4723
+ void storage.setItem(key, JSON.stringify(payload)).catch(() => {
4724
+ });
4725
+ } catch {
4726
+ }
4727
+ };
4728
+ const isCountedOpenEvent = (event) => typeof event.question_key === "string" && event.question_key.startsWith("app.");
4729
+ const enforceSizeCap = () => {
4730
+ let overflow = pending.length - maxSize;
4731
+ if (overflow <= 0) return;
4732
+ const kept = [];
4733
+ for (const item of pending) {
4734
+ if (overflow > 0 && !isCountedOpenEvent(item.event)) {
4735
+ overflow--;
4736
+ continue;
4737
+ }
4738
+ kept.push(item);
4739
+ }
4740
+ if (overflow > 0) kept.splice(0, overflow);
4741
+ pending = kept;
4742
+ };
4743
+ const safeSig = (event) => {
4744
+ try {
4745
+ return JSON.stringify(event);
4746
+ } catch {
4747
+ return `__nosig_${nextId}_${Math.random()}`;
4748
+ }
4749
+ };
4750
+ const mergePersisted = (persistedItems) => {
4751
+ if (persistedItems.length === 0) return;
4752
+ const seen = new Set(pending.map((item) => item.sig));
4753
+ const restored = [];
4754
+ for (const persisted of persistedItems) {
4755
+ const sig = safeSig(persisted.event);
4756
+ if (seen.has(sig)) continue;
4757
+ seen.add(sig);
4758
+ restored.push({ id: nextId++, event: persisted.event, sig });
4759
+ }
4760
+ if (restored.length === 0) return;
4761
+ pending = [...restored, ...pending];
4762
+ enforceSizeCap();
4763
+ persist();
4764
+ };
4765
+ const loadPromise = (async () => {
4766
+ if (!storage) return;
4767
+ try {
4768
+ const read = Promise.resolve(storage.getItem(key));
4769
+ read.catch(() => {
4770
+ });
4771
+ const raced = await withTimeout3(read, READ_TIMEOUT_MS3);
4772
+ if (raced === READ_TIMED_OUT2) {
4773
+ backlogUnread = true;
4774
+ warnInDev(
4775
+ `[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.`
4776
+ );
4777
+ void read.then((late) => {
4778
+ backlogUnread = false;
4779
+ mergePersisted(parsePersisted2(late));
4780
+ flush();
4781
+ }).catch(() => {
4782
+ backlogUnread = false;
4783
+ persist();
4784
+ });
4785
+ return;
4786
+ }
4787
+ backlogUnread = false;
4788
+ mergePersisted(parsePersisted2(raced));
4789
+ persist();
4790
+ } catch {
4791
+ backlogUnread = false;
4792
+ persist();
4793
+ }
4794
+ })();
4795
+ const postBatch = async (events) => {
4796
+ const req = buildEventsRequest(target, events);
4797
+ if (!req) return false;
4798
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
4799
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), 15e3);
4800
+ try {
4801
+ const res = await fetch(req.url, { ...req.init, signal: controller == null ? void 0 : controller.signal });
4802
+ warnOnSkippedEvents(res);
4803
+ return !!(res && res.ok);
4804
+ } catch {
4805
+ return false;
4806
+ } finally {
4807
+ clearTimeout(timer);
4808
+ }
4809
+ };
4810
+ const clearRetry = () => {
4811
+ if (retryTimer !== void 0) {
4812
+ clearTimeout(retryTimer);
4813
+ retryTimer = void 0;
4814
+ }
4815
+ };
4816
+ const scheduleRetry = () => {
4817
+ if (attempt >= maxRetries) return;
4818
+ const delay = Math.min(baseBackoffMs * 2 ** attempt, maxBackoffMs);
4819
+ attempt++;
4820
+ clearRetry();
4821
+ retryTimer = setTimeout(() => {
4822
+ retryTimer = void 0;
4823
+ void drain();
4824
+ }, delay);
4825
+ unrefTimer(retryTimer);
4826
+ };
4827
+ const drain = async () => {
4828
+ if (disposed) return;
4829
+ try {
4830
+ await loadPromise;
4831
+ } catch {
4832
+ }
4833
+ if (disposed) return;
4834
+ if (flushing) return;
4835
+ flushing = true;
4836
+ try {
4837
+ while (pending.length > 0) {
4838
+ const batch = pending.slice(0, batchSize);
4839
+ const ok = await postBatch(batch.map((item) => item.event));
4840
+ if (!ok) {
4841
+ scheduleRetry();
4842
+ return;
4843
+ }
4844
+ const acked = new Set(batch.map((item) => item.id));
4845
+ pending = pending.filter((item) => !acked.has(item.id));
4846
+ persist();
4847
+ attempt = 0;
4848
+ clearRetry();
4849
+ }
4850
+ } finally {
4851
+ flushing = false;
4852
+ }
4853
+ };
4854
+ const flush = () => {
4855
+ try {
4856
+ void drain();
4857
+ } catch {
4858
+ }
4859
+ };
4860
+ const enqueue = (event) => {
4861
+ if (disposed) return;
4862
+ try {
4863
+ const stamped = stamp(event);
4864
+ const sig = safeSig(stamped);
4865
+ for (const item of pending) {
4866
+ if (item.sig === sig) return;
4867
+ }
4868
+ pending.push({ id: nextId++, event: stamped, sig });
4869
+ enforceSizeCap();
4870
+ persist();
4871
+ if (retryTimer === void 0) flush();
4872
+ } catch {
4873
+ }
4874
+ };
4875
+ const notifyOnline = () => {
4876
+ attempt = 0;
4877
+ clearRetry();
4878
+ flush();
4879
+ };
4880
+ const size = () => pending.length;
4881
+ const dispose = () => {
4882
+ if (disposed) return;
4883
+ disposed = true;
4884
+ clearRetry();
4885
+ if (storage) releaseQueueKey(key);
4886
+ };
4887
+ return { enqueue, flush, notifyOnline, size, dispose };
4888
+ };
4889
+
4890
+ // src/activation/revalidation.ts
4891
+ var REVALIDATION_SLOT = /* @__PURE__ */ Symbol.for(
4892
+ "@wireai/activation:activationRevalidation"
4893
+ );
4894
+ var globalSlot2 = globalThis;
4895
+ var store = () => {
4896
+ const existing = globalSlot2[REVALIDATION_SLOT];
4897
+ if (existing) return existing;
4898
+ const created = { version: 0, listeners: /* @__PURE__ */ new Set() };
4899
+ globalSlot2[REVALIDATION_SLOT] = created;
4900
+ return created;
4901
+ };
4902
+ var bumpActivationRevalidation = () => {
4903
+ const s = store();
4904
+ s.version += 1;
4905
+ for (const listener of Array.from(s.listeners)) {
4906
+ try {
4907
+ listener();
4908
+ } catch {
4909
+ }
4910
+ }
4911
+ };
4912
+ var subscribeActivationRevalidation = (listener) => {
4913
+ const s = store();
4914
+ s.listeners.add(listener);
4915
+ return () => {
4916
+ s.listeners.delete(listener);
4917
+ };
4918
+ };
4919
+ var getActivationRevalidationVersion = () => store().version;
4920
+ var resetActivationRevalidation = () => {
4921
+ const s = store();
4922
+ s.version = 0;
4923
+ s.listeners.clear();
4924
+ };
4925
+
4926
+ // src/activation/wireActivation.ts
4927
+ var clean = cleanString;
4928
+ var createWireActivation = (config) => {
4929
+ var _a, _b;
4930
+ const target = { serverUrl: config.serverUrl, apiKey: config.apiKey };
4931
+ const explicitDeviceKey = (_b = clean(config.deviceKey)) != null ? _b : clean((_a = config.userContext) == null ? void 0 : _a.deviceKey);
4932
+ resolveIdentity({
4933
+ value: explicitDeviceKey,
4934
+ space: "device",
4935
+ source: "host",
4936
+ scope: config.appId
4937
+ });
4938
+ const autoDeviceKeyOptions = {
4939
+ appId: config.appId,
4940
+ // An explicit key opts out of minting AND persisting (unchanged contract).
4941
+ storage: explicitDeviceKey ? void 0 : config.storage
4942
+ };
4943
+ if (!explicitDeviceKey) resolveAutoDeviceKey(autoDeviceKeyOptions);
4944
+ const detectedAppVersion = detectAppVersion();
4945
+ const applyContext = (event) => {
4946
+ var _a2, _b2, _c;
4947
+ const resolved = resolveUserContext(
4948
+ // `??` is lazy on purpose: an explicit key must never even touch the auto registry.
4949
+ {
4950
+ ...(_a2 = config.userContext) != null ? _a2 : {},
4951
+ deviceKey: explicitDeviceKey != null ? explicitDeviceKey : resolveAutoDeviceKey(autoDeviceKeyOptions)
4952
+ },
4953
+ { autoAppVersion: (_b2 = config.appVersion) != null ? _b2 : detectedAppVersion }
4954
+ );
4955
+ if (resolved.userContext) {
4956
+ event.user_context = { ...resolved.userContext, ...(_c = event.user_context) != null ? _c : {} };
4957
+ }
4958
+ if (resolved.userId && !event.user_id) event.user_id = resolved.userId;
4959
+ };
4960
+ let durability;
4961
+ const bufferFailedEvent = (event) => {
4962
+ var _a2;
4963
+ if (config.sink) {
4964
+ config.sink(event);
4965
+ return;
4966
+ }
4967
+ if (!durability) {
4968
+ durability = createEventQueue({
4969
+ target,
4970
+ storage: config.storage,
4971
+ storageKey: `wireai:evtq:activation:${(_a2 = config.appId) != null ? _a2 : "default"}`
4972
+ });
4973
+ }
4974
+ durability.enqueue(event);
4975
+ };
4976
+ const track = async (name, meta) => {
4977
+ if (!clean(name)) return false;
4978
+ const sessionId = ensureCurrentSessionId();
4979
+ const event = {
4980
+ event_type: "app_event",
4981
+ session_id: sessionId,
4982
+ question_key: name
4983
+ };
4984
+ if (meta && Object.keys(meta).length > 0) event.meta = JSON.stringify(meta);
4985
+ applyContext(event);
4986
+ const outcome = await reportClientEventsOutcome(target, [event]);
4987
+ const ok = outcome === "delivered";
4988
+ if (ok) bumpActivationRevalidation();
4989
+ if (outcome === "unreachable") bufferFailedEvent(event);
4990
+ return ok;
4991
+ };
4992
+ return {
4993
+ track,
4994
+ get sessionId() {
4995
+ return getCurrentSessionId();
4996
+ },
4997
+ subscribeRevalidation: subscribeActivationRevalidation,
4998
+ getRevalidationVersion: getActivationRevalidationVersion,
4999
+ dispose: () => {
5000
+ durability == null ? void 0 : durability.dispose();
5001
+ durability = void 0;
5002
+ }
5003
+ };
5004
+ };
5005
+ var useActivationRevalidation = () => React19.useSyncExternalStore(
5006
+ subscribeActivationRevalidation,
5007
+ getActivationRevalidationVersion,
5008
+ getActivationRevalidationVersion
5009
+ );
5010
+ var useWireActivation = (config) => {
5011
+ var _a, _b;
5012
+ const ref = React19.useRef(void 0);
5013
+ const prevKeys = React19.useRef("");
5014
+ const currentKeys = [
5015
+ config.serverUrl,
5016
+ config.apiKey,
5017
+ config.appId,
5018
+ config.deviceKey,
5019
+ (_a = config.userContext) == null ? void 0 : _a.deviceKey
5020
+ ].join("|");
5021
+ if (!ref.current || prevKeys.current !== currentKeys) {
5022
+ prevKeys.current = currentKeys;
5023
+ (_b = ref.current) == null ? void 0 : _b.dispose();
5024
+ ref.current = createWireActivation(config);
5025
+ }
5026
+ React19.useEffect(
5027
+ () => () => {
5028
+ var _a2;
5029
+ (_a2 = ref.current) == null ? void 0 : _a2.dispose();
5030
+ ref.current = void 0;
5031
+ },
5032
+ []
5033
+ );
5034
+ const revalidation = useActivationRevalidation();
5035
+ return { track: ref.current.track, sessionId: ref.current.sessionId, revalidation };
5036
+ };
5037
+
5038
+ // src/revenuecat/purchaseEvents.ts
5039
+ var WIRE_PURCHASE_EVENTS = {
5040
+ /** The paywall became visible (offerings loaded). */
5041
+ paywallShown: "wire_paywall_shown",
5042
+ /** The user tapped buy; the store sheet is about to open. */
5043
+ checkoutStarted: "wire_checkout_started",
5044
+ /** The store confirmed the purchase AND the entitlement is now active. */
5045
+ purchased: "wire_purchase_completed",
5046
+ /** The purchase did not land: a user cancel or a real store/network error (see `reason`). */
5047
+ purchaseFailed: "wire_purchase_failed",
5048
+ /** A restore ran (whether or not it produced an entitlement; see `plan_tier`). */
5049
+ restored: "wire_purchase_restored"
5050
+ };
5051
+ var PLAN_TIER_CONTEXT_KEY = "plan_tier";
5052
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
5053
+ var activeEntitlement = (info, entitlementId) => {
5054
+ var _a, _b;
5055
+ const active = asRecord((_b = asRecord((_a = asRecord(info)) == null ? void 0 : _a.entitlements)) == null ? void 0 : _b.active);
5056
+ const found = active == null ? void 0 : active[entitlementId];
5057
+ const entitlement = asRecord(found);
5058
+ if (!entitlement) return void 0;
5059
+ return entitlement.isActive === false ? void 0 : entitlement;
5060
+ };
5061
+ var isTrial = (entitlement) => typeof entitlement.periodType === "string" && entitlement.periodType.toUpperCase() === "TRIAL";
5062
+ var resolvePlanTier = (info, entitlementId) => {
5063
+ const entitlement = activeEntitlement(info, entitlementId);
5064
+ if (!entitlement) return "free";
5065
+ return isTrial(entitlement) ? "trial" : "paid";
5066
+ };
5067
+ var describePackage = (pkg) => {
5068
+ const props = {};
5069
+ if (!pkg) return props;
5070
+ if (typeof pkg.identifier === "string") props.package_id = pkg.identifier;
5071
+ if (typeof pkg.packageType === "string") props.package_type = pkg.packageType;
5072
+ if (typeof pkg.offeringIdentifier === "string") props.offering_id = pkg.offeringIdentifier;
5073
+ const product = asRecord(pkg.product);
5074
+ if (product) {
5075
+ if (typeof product.identifier === "string") props.product_id = product.identifier;
5076
+ if (typeof product.price === "number" && Number.isFinite(product.price)) {
5077
+ props.price = product.price;
5078
+ }
5079
+ if (typeof product.currencyCode === "string") props.currency = product.currencyCode;
5080
+ }
5081
+ return props;
5082
+ };
5083
+ var describeEntitlement = (info, entitlementId) => {
5084
+ const entitlement = activeEntitlement(info, entitlementId);
5085
+ const props = {
5086
+ entitlement: entitlementId,
5087
+ plan_tier: entitlement ? isTrial(entitlement) ? "trial" : "paid" : "free"
5088
+ };
5089
+ if (!entitlement) return props;
5090
+ if (typeof entitlement.periodType === "string") props.period_type = entitlement.periodType;
5091
+ props.is_trial = isTrial(entitlement);
5092
+ if (typeof entitlement.willRenew === "boolean") props.will_renew = entitlement.willRenew;
5093
+ if (typeof entitlement.store === "string") props.store = entitlement.store;
5094
+ if (typeof entitlement.productIdentifier === "string") {
5095
+ props.product_id = entitlement.productIdentifier;
5096
+ }
5097
+ if (typeof entitlement.isSandbox === "boolean") props.is_sandbox = entitlement.isSandbox;
5098
+ return props;
5099
+ };
5100
+ var PURCHASE_CANCELLED_CODE = "1";
5101
+ var isUserCancelled = (error) => {
5102
+ const record = asRecord(error);
5103
+ if (!record) return false;
5104
+ if (record.userCancelled === true) return true;
5105
+ return record.code === PURCHASE_CANCELLED_CODE;
5106
+ };
5107
+ var describeFailure = (error) => {
5108
+ const record = asRecord(error);
5109
+ const code = typeof (record == null ? void 0 : record.code) === "string" ? record.code : "unknown";
5110
+ return { reason: isUserCancelled(error) ? "cancelled" : "error", code };
5111
+ };
5112
+
5113
+ // src/revenuecat/revenueCatBridge.ts
5114
+ var createRevenueCatBridge = (config) => {
5115
+ const { analytics, entitlementId } = config;
5116
+ const writeTier = config.writePlanTier !== false;
5117
+ const emit = (event, props) => {
5118
+ try {
5119
+ const result = analytics.track(event, { ...config.commonProps, ...props });
4799
5120
  if (result && typeof result.catch === "function") {
4800
5121
  void result.catch(() => {
4801
5122
  });
@@ -4906,15 +5227,9 @@ var reportSessionStart = (opts) => {
4906
5227
  opts.sink(event);
4907
5228
  return;
4908
5229
  }
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) => {
5230
+ const req = buildEventsRequest(target, [event]);
5231
+ if (!req) return;
5232
+ void fetch(req.url, req.init).then((res) => {
4918
5233
  warnOnSkippedEvents(res);
4919
5234
  }).catch(() => {
4920
5235
  });
@@ -4926,13 +5241,25 @@ var useSessionStart = (config, options = {}) => {
4926
5241
  const latest = React19.useRef({ config, options });
4927
5242
  latest.current = { config, options };
4928
5243
  React19.useEffect(() => {
4929
- const resolveDeviceKey = (cfg, opts) => {
5244
+ let cancelled = false;
5245
+ const resolveDeviceKey = (opts, autoDeviceKey) => {
4930
5246
  const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
4931
5247
  if (host) return host;
4932
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
4933
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5248
+ return autoDeviceKey;
5249
+ };
5250
+ const openAutoDeviceKey = (fireOpen) => {
5251
+ const { config: cfg, options: opts } = latest.current;
5252
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5253
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5254
+ fireOpen(void 0);
5255
+ return;
5256
+ }
5257
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5258
+ if (cancelled) return;
5259
+ fireOpen((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5260
+ });
4934
5261
  };
4935
- const fire = () => {
5262
+ const fire = (autoDeviceKey) => {
4936
5263
  var _a, _b;
4937
5264
  const { config: cfg, options: opts } = latest.current;
4938
5265
  if (!(cfg == null ? void 0 : cfg.serverUrl)) return;
@@ -4944,7 +5271,7 @@ var useSessionStart = (config, options = {}) => {
4944
5271
  target,
4945
5272
  // A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
4946
5273
  userId: opts.userId,
4947
- deviceKey: resolveDeviceKey(cfg, opts),
5274
+ deviceKey: resolveDeviceKey(opts, autoDeviceKey),
4948
5275
  sessionCount: opts.sessionCount,
4949
5276
  appVersion: (_b = cfg.appVersion) != null ? _b : device.appVersion,
4950
5277
  platform: reactNative.Platform.OS,
@@ -4952,7 +5279,7 @@ var useSessionStart = (config, options = {}) => {
4952
5279
  meta: opts.meta
4953
5280
  });
4954
5281
  };
4955
- fire();
5282
+ openAutoDeviceKey(fire);
4956
5283
  let backgroundedAt = null;
4957
5284
  const onChange = (state) => {
4958
5285
  if (state === "background" || state === "inactive") {
@@ -4962,11 +5289,12 @@ var useSessionStart = (config, options = {}) => {
4962
5289
  if (state === "active") {
4963
5290
  const since = backgroundedAt;
4964
5291
  backgroundedAt = null;
4965
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fire();
5292
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) openAutoDeviceKey(fire);
4966
5293
  }
4967
5294
  };
4968
5295
  const sub = reactNative.AppState.addEventListener("change", onChange);
4969
5296
  return () => {
5297
+ cancelled = true;
4970
5298
  if (sub && typeof sub.remove === "function") sub.remove();
4971
5299
  };
4972
5300
  }, []);
@@ -4975,11 +5303,12 @@ var useSessionStart = (config, options = {}) => {
4975
5303
  // src/session-analytics/lifecycle.ts
4976
5304
  var FIRST_OPEN_EVENT = "app.first_open";
4977
5305
  var firstOpenStorageKey = (appId) => `wireai:first_open:${appId}`;
4978
- var READ_TIMEOUT_MS3 = 1500;
4979
- var withTimeout3 = (p, ms) => {
5306
+ var READ_TIMEOUT_MS4 = 1500;
5307
+ var READ_TIMED_OUT3 = /* @__PURE__ */ Symbol("wireai:first-open-read-timeout");
5308
+ var withTimeout4 = (p, ms) => {
4980
5309
  let timer;
4981
5310
  const timeout = new Promise((resolve) => {
4982
- timer = setTimeout(() => resolve(void 0), ms);
5311
+ timer = setTimeout(() => resolve(READ_TIMED_OUT3), ms);
4983
5312
  });
4984
5313
  return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
4985
5314
  };
@@ -4998,335 +5327,82 @@ var buildLifecycleEvent = (questionKey, opts) => {
4998
5327
  if (opts.appVersion) userContext.app_version = opts.appVersion;
4999
5328
  if (opts.platform) userContext.platform = opts.platform;
5000
5329
  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();
5330
+ event_type: "app_event",
5331
+ question_key: questionKey,
5332
+ session_id: (_a = opts.sessionId) != null ? _a : makeSessionId()
5333
+ };
5334
+ const userId = sanitizeUserId(opts.userId);
5335
+ if (userId) event.user_id = userId;
5336
+ if (Object.keys(userContext).length > 0) event.user_context = userContext;
5337
+ if (opts.device) event.device = opts.device;
5338
+ if (opts.meta && Object.keys(opts.meta).length > 0) event.meta = JSON.stringify(opts.meta);
5339
+ return event;
5116
5340
  };
5117
- var parsePersisted2 = (raw) => {
5118
- if (!raw) return [];
5341
+ var routeLifecycleEvent = (event, opts) => {
5119
5342
  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
- }
5343
+ if (opts.sink) {
5344
+ opts.sink(event);
5345
+ return;
5127
5346
  }
5128
- return items;
5347
+ const req = buildEventsRequest(opts.target, [event]);
5348
+ if (!req) return;
5349
+ void fetch(req.url, req.init).catch(() => {
5350
+ });
5129
5351
  } catch {
5130
- return [];
5131
5352
  }
5132
5353
  };
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;
5354
+ var emitFirstOpen = (opts) => {
5355
+ routeLifecycleEvent(buildLifecycleEvent(FIRST_OPEN_EVENT, opts), opts);
5356
+ };
5357
+ var reportFirstOpen = (opts) => {
5358
+ var _a;
5359
+ const appId = (_a = opts.appId) != null ? _a : "default";
5360
+ if (_firstOpenLatched.has(appId)) return;
5361
+ _firstOpenLatched.add(appId);
5362
+ const storage = opts.storage;
5363
+ if (!storage) {
5364
+ emitFirstOpen(opts);
5365
+ return;
5366
+ }
5367
+ const key = firstOpenStorageKey(appId);
5368
+ void (async () => {
5214
5369
  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();
5370
+ const seen = await withTimeout4(storage.getItem(key), READ_TIMEOUT_MS4);
5371
+ if (seen === READ_TIMED_OUT3) return;
5372
+ if (seen) return;
5373
+ emitFirstOpen(opts);
5374
+ try {
5375
+ void storage.setItem(key, JSON.stringify({ ts: Date.now() })).catch(() => {
5231
5376
  });
5232
- return;
5377
+ } catch {
5233
5378
  }
5234
- mergePersisted(parsePersisted2(raced));
5235
5379
  } catch {
5236
5380
  }
5237
5381
  })();
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
5382
  };
5323
-
5324
- // src/session-analytics/useLifecycleEvents.ts
5383
+ var wireLifecycleEvents = (opts) => {
5384
+ var _a;
5385
+ const sessionId = (_a = opts.sessionId) != null ? _a : makeSessionId();
5386
+ reportFirstOpen({ ...opts, sessionId });
5387
+ reportSessionStart({
5388
+ target: opts.target,
5389
+ sink: opts.sink,
5390
+ sessionId,
5391
+ userId: opts.userId,
5392
+ deviceKey: opts.deviceKey,
5393
+ sessionCount: opts.sessionCount,
5394
+ appVersion: opts.appVersion,
5395
+ platform: opts.platform,
5396
+ device: opts.device,
5397
+ meta: opts.meta
5398
+ });
5399
+ };
5325
5400
  var useLifecycleEvents = (config, options = {}) => {
5326
5401
  const latest = React19.useRef({ config, options });
5327
5402
  latest.current = { config, options };
5328
5403
  const queueRef = React19.useRef(void 0);
5329
5404
  React19.useEffect(() => {
5405
+ let cancelled = false;
5330
5406
  const resolveSink = () => {
5331
5407
  var _a, _b;
5332
5408
  const { config: cfg, options: opts } = latest.current;
@@ -5347,7 +5423,7 @@ var useLifecycleEvents = (config, options = {}) => {
5347
5423
  var _a;
5348
5424
  return (cfg == null ? void 0 : cfg.serverUrl) ? { serverUrl: cfg.serverUrl, apiKey: (_a = cfg.apiKey) != null ? _a : "" } : void 0;
5349
5425
  };
5350
- const resolveDeviceKey = (cfg, opts) => {
5426
+ const resolveDeviceKey = (cfg, opts, autoDeviceKey) => {
5351
5427
  var _a;
5352
5428
  const host = (_a = resolveIdentity({
5353
5429
  value: opts.deviceKey,
@@ -5356,11 +5432,22 @@ var useLifecycleEvents = (config, options = {}) => {
5356
5432
  scope: cfg == null ? void 0 : cfg.appId
5357
5433
  })) == null ? void 0 : _a.value;
5358
5434
  if (host) return host;
5359
- if (!(cfg == null ? void 0 : cfg.storage)) return void 0;
5360
- return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
5435
+ return autoDeviceKey;
5436
+ };
5437
+ const openAutoDeviceKey = (fire) => {
5438
+ const { config: cfg, options: opts } = latest.current;
5439
+ const hostKey = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : void 0;
5440
+ if (hostKey || !(cfg == null ? void 0 : cfg.storage)) {
5441
+ fire(void 0);
5442
+ return;
5443
+ }
5444
+ void hydrateDeviceIdentity({ appId: cfg.appId, storage: cfg.storage }).then((identity) => {
5445
+ if (cancelled) return;
5446
+ fire((identity == null ? void 0 : identity.durable) ? identity.value : void 0);
5447
+ });
5361
5448
  };
5362
5449
  const mountOpenSessionId = makeSessionId();
5363
- const fireSession = (sessionId) => {
5450
+ const fireSession = (sessionId, autoDeviceKey) => {
5364
5451
  var _a;
5365
5452
  const { config: cfg, options: opts } = latest.current;
5366
5453
  if (opts.enabled === false) return;
@@ -5372,7 +5459,7 @@ var useLifecycleEvents = (config, options = {}) => {
5372
5459
  sink: resolveSink(),
5373
5460
  sessionId,
5374
5461
  userId: opts.userId,
5375
- deviceKey: resolveDeviceKey(cfg, opts),
5462
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5376
5463
  sessionCount: opts.sessionCount,
5377
5464
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5378
5465
  platform: reactNative.Platform.OS,
@@ -5380,9 +5467,9 @@ var useLifecycleEvents = (config, options = {}) => {
5380
5467
  meta: opts.meta
5381
5468
  });
5382
5469
  };
5383
- const fireMountOpen = () => {
5470
+ const fireMountOpen = (autoDeviceKey) => {
5384
5471
  var _a;
5385
- fireSession(mountOpenSessionId);
5472
+ fireSession(mountOpenSessionId, autoDeviceKey);
5386
5473
  const { config: cfg, options: opts } = latest.current;
5387
5474
  if (opts.enabled === false) return;
5388
5475
  const device = collectDeviceContext();
@@ -5394,7 +5481,7 @@ var useLifecycleEvents = (config, options = {}) => {
5394
5481
  storage: cfg == null ? void 0 : cfg.storage,
5395
5482
  appId: cfg == null ? void 0 : cfg.appId,
5396
5483
  userId: opts.userId,
5397
- deviceKey: resolveDeviceKey(cfg, opts),
5484
+ deviceKey: resolveDeviceKey(cfg, opts, autoDeviceKey),
5398
5485
  sessionCount: opts.sessionCount,
5399
5486
  appVersion: (_a = cfg == null ? void 0 : cfg.appVersion) != null ? _a : device.appVersion,
5400
5487
  platform: reactNative.Platform.OS,
@@ -5402,16 +5489,7 @@ var useLifecycleEvents = (config, options = {}) => {
5402
5489
  meta: opts.meta
5403
5490
  });
5404
5491
  };
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
- }
5492
+ openAutoDeviceKey(fireMountOpen);
5415
5493
  let backgroundedAt = null;
5416
5494
  const onChange = (state) => {
5417
5495
  if (state === "background" || state === "inactive") {
@@ -5421,13 +5499,18 @@ var useLifecycleEvents = (config, options = {}) => {
5421
5499
  if (state === "active") {
5422
5500
  const since = backgroundedAt;
5423
5501
  backgroundedAt = null;
5424
- if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fireSession();
5502
+ if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) {
5503
+ openAutoDeviceKey((autoDeviceKey) => fireSession(void 0, autoDeviceKey));
5504
+ }
5425
5505
  }
5426
5506
  };
5427
5507
  const sub = reactNative.AppState.addEventListener("change", onChange);
5428
5508
  return () => {
5509
+ var _a;
5429
5510
  cancelled = true;
5430
5511
  if (sub && typeof sub.remove === "function") sub.remove();
5512
+ (_a = queueRef.current) == null ? void 0 : _a.dispose();
5513
+ queueRef.current = void 0;
5431
5514
  };
5432
5515
  }, []);
5433
5516
  };