@scalebun/react-native 1.10.7 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
  2. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
  3. package/dist/scalebun.full.js +467 -255
  4. package/dist/scalebun.slim.js +466 -254
  5. package/ios/Capture/InteractionTracker.swift +8 -4
  6. package/ios/Ota/OtaSlotManager.swift +19 -5
  7. package/lib/commonjs/analytics/EventTracker.js +5 -5
  8. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  9. package/lib/commonjs/core/constants/version.js +7 -2
  10. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
  11. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  12. package/lib/commonjs/features/journey/uiState.js +8 -1
  13. package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
  14. package/lib/commonjs/features/ota/OtaTypes.js +4 -0
  15. package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
  16. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  17. package/lib/commonjs/features/session/SessionManager.js +37 -38
  18. package/lib/commonjs/public/ScaleBunFacade.js +115 -2
  19. package/lib/module/analytics/EventTracker.js +5 -5
  20. package/lib/module/analytics/automaticEvents.js +3 -2
  21. package/lib/module/core/constants/version.js +7 -2
  22. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
  23. package/lib/module/features/journey/interactionProtocol.js +38 -0
  24. package/lib/module/features/journey/uiState.js +8 -1
  25. package/lib/module/features/ota/OtaOrchestrator.js +174 -48
  26. package/lib/module/features/ota/OtaTypes.js +1 -1
  27. package/lib/module/features/ota/useOtaUpdate.js +11 -2
  28. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  29. package/lib/module/features/session/SessionManager.js +37 -38
  30. package/lib/module/public/ScaleBunFacade.js +115 -2
  31. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  32. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  33. package/lib/typescript/core/constants/version.d.ts +7 -2
  34. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  35. package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
  36. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
  37. package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
  38. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  39. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  40. package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
  41. package/package.json +4 -3
  42. package/src/analytics/EventTracker.ts +5 -5
  43. package/src/analytics/automaticEvents.ts +4 -0
  44. package/src/core/constants/version.ts +7 -2
  45. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
  46. package/src/features/journey/interactionProtocol.ts +65 -0
  47. package/src/features/journey/uiState.ts +9 -4
  48. package/src/features/ota/OtaEventEmitter.ts +12 -0
  49. package/src/features/ota/OtaOrchestrator.ts +209 -62
  50. package/src/features/ota/OtaTypes.ts +37 -39
  51. package/src/features/ota/useOtaUpdate.ts +11 -2
  52. package/src/features/session/JourneyEventPipeline.ts +7 -5
  53. package/src/features/session/SessionManager.ts +75 -38
  54. package/src/public/ScaleBunFacade.ts +127 -3
@@ -605,7 +605,7 @@ var SDK_VERSION;
605
605
  var init_version = __esm({
606
606
  "lib/module/core/constants/version.js"() {
607
607
  "use strict";
608
- SDK_VERSION = "1.10.7";
608
+ SDK_VERSION = "1.11.1";
609
609
  }
610
610
  });
611
611
 
@@ -4567,22 +4567,23 @@ var init_JourneyEventPipeline = __esm({
4567
4567
  emit(type, opts) {
4568
4568
  try {
4569
4569
  const key = `${type}:${opts?.subtype ?? ""}`;
4570
- const now2 = Date.now();
4571
- if (key === this.lastEventKey && now2 - this.lastEventTs < this.config.dedupeWindowMs) {
4570
+ const receivedAt = Date.now();
4571
+ const occurredAt = opts?.timestamp ?? receivedAt;
4572
+ if (key === this.lastEventKey && receivedAt - this.lastEventTs < this.config.dedupeWindowMs) {
4572
4573
  const incomingConfidence = opts?.payload?.confidence;
4573
4574
  if (incomingConfidence === "high" && this.lastEventConfidence !== "high") {
4574
- this._replaceLastEvent(key, now2, opts);
4575
+ this._replaceLastEvent(key, receivedAt, opts);
4575
4576
  }
4576
4577
  return null;
4577
4578
  }
4578
4579
  this.lastEventKey = key;
4579
- this.lastEventTs = now2;
4580
+ this.lastEventTs = receivedAt;
4580
4581
  this.lastEventConfidence = opts?.payload?.confidence ?? null;
4581
4582
  const event = {
4582
4583
  eventId: generateEventId2(),
4583
4584
  sessionId: this.sessionId,
4584
4585
  journeyId: opts?.journeyId,
4585
- ts: now2,
4586
+ ts: occurredAt,
4586
4587
  type,
4587
4588
  subtype: opts?.subtype,
4588
4589
  severity: opts?.severity ?? inferSeverity(type),
@@ -5682,6 +5683,99 @@ var init_calibrationContext = __esm({
5682
5683
  }
5683
5684
  });
5684
5685
 
5686
+ // lib/module/analytics/automaticEvents.js
5687
+ function compactProperties(properties) {
5688
+ const out = {
5689
+ capture_source: "automatic"
5690
+ };
5691
+ for (const [key, value] of Object.entries(properties ?? {})) {
5692
+ if (value !== void 0) out[key] = value;
5693
+ }
5694
+ return out;
5695
+ }
5696
+ function emitAutomaticEvent(name, properties, timestamp) {
5697
+ const event = {
5698
+ name,
5699
+ properties: compactProperties(properties),
5700
+ timestamp
5701
+ };
5702
+ if (listeners.size === 0) {
5703
+ pending2.push(event);
5704
+ if (pending2.length > MAX_PENDING) pending2.shift();
5705
+ return;
5706
+ }
5707
+ for (const listener of listeners) {
5708
+ try {
5709
+ listener(event);
5710
+ } catch {
5711
+ }
5712
+ }
5713
+ }
5714
+ function subscribeAutomaticEvents(listener) {
5715
+ listeners.add(listener);
5716
+ if (pending2.length > 0) {
5717
+ const buffered = pending2.splice(0, pending2.length);
5718
+ for (const event of buffered) {
5719
+ try {
5720
+ listener(event);
5721
+ } catch {
5722
+ }
5723
+ }
5724
+ }
5725
+ return () => {
5726
+ listeners.delete(listener);
5727
+ };
5728
+ }
5729
+ var listeners, pending2, MAX_PENDING;
5730
+ var init_automaticEvents = __esm({
5731
+ "lib/module/analytics/automaticEvents.js"() {
5732
+ "use strict";
5733
+ listeners = /* @__PURE__ */ new Set();
5734
+ pending2 = [];
5735
+ MAX_PENDING = 50;
5736
+ }
5737
+ });
5738
+
5739
+ // lib/module/features/journey/interactionProtocol.js
5740
+ function generateInteractionId() {
5741
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
5742
+ }
5743
+ function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
5744
+ let best;
5745
+ let bestDelta = toleranceMs + 1;
5746
+ for (const start of starts) {
5747
+ const delta = Math.abs(start.occurredAt - occurredAt);
5748
+ if (delta < bestDelta) {
5749
+ best = start;
5750
+ bestDelta = delta;
5751
+ }
5752
+ }
5753
+ return bestDelta <= toleranceMs ? best : void 0;
5754
+ }
5755
+ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5756
+ return {
5757
+ gesture_type: payload.gestureType,
5758
+ screen_name: screenName,
5759
+ interaction_id: payload.interaction_id,
5760
+ interaction_protocol: payload.interaction_protocol,
5761
+ state_status: payload.state_status,
5762
+ ui: payload.ui,
5763
+ target_id: payload.target_id,
5764
+ normalized_x: payload.normalizedX,
5765
+ normalized_y: payload.normalizedY,
5766
+ direction: payload.direction,
5767
+ duration_ms: payload.durationMs,
5768
+ canonical_mirror: canonicalMirror || void 0
5769
+ };
5770
+ }
5771
+ var INTERACTION_PROTOCOL_VERSION;
5772
+ var init_interactionProtocol = __esm({
5773
+ "lib/module/features/journey/interactionProtocol.js"() {
5774
+ "use strict";
5775
+ INTERACTION_PROTOCOL_VERSION = 1;
5776
+ }
5777
+ });
5778
+
5685
5779
  // lib/module/features/session/SessionManager.js
5686
5780
  var SessionManager_exports = {};
5687
5781
  __export(SessionManager_exports, {
@@ -5690,7 +5784,7 @@ __export(SessionManager_exports, {
5690
5784
  function generateSessionId2() {
5691
5785
  return `ses-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
5692
5786
  }
5693
- var HEATMAP_MAX_INTERACTIONS_PER_WINDOW, _instance2, SessionManager;
5787
+ var _instance2, SessionManager;
5694
5788
  var init_SessionManager = __esm({
5695
5789
  "lib/module/features/session/SessionManager.js"() {
5696
5790
  "use strict";
@@ -5707,7 +5801,8 @@ var init_SessionManager = __esm({
5707
5801
  init_redaction();
5708
5802
  init_calibrationContext();
5709
5803
  init_device();
5710
- HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
5804
+ init_automaticEvents();
5805
+ init_interactionProtocol();
5711
5806
  _instance2 = null;
5712
5807
  SessionManager = class _SessionManager {
5713
5808
  desktopTransport = null;
@@ -5718,15 +5813,6 @@ var init_SessionManager = __esm({
5718
5813
  * lane even when no replay recording is active. Additive, opt-in (default off).
5719
5814
  */
5720
5815
  _captureInteractionHeatmap = false;
5721
- /**
5722
- * Sampling cap for the analytics-lane heatmap emission. Now that capture is
5723
- * ON by default, an unbounded one-event-per-gesture stream could materially
5724
- * inflate ingest volume. We cap emitted interactions per analytics-session
5725
- * window (finalize-scoped per foreground): the first N gestures define the
5726
- * hotspot shape; the long tail is dropped. Resets when the window changes.
5727
- */
5728
- _heatmapWindowSessionId = null;
5729
- _heatmapWindowCount = 0;
5730
5816
  session = null;
5731
5817
  active = false;
5732
5818
  timeoutTimer = null;
@@ -6207,18 +6293,11 @@ var init_SessionManager = __esm({
6207
6293
  * injected sensitive keys (e.g. a label) are stripped before transport.
6208
6294
  * - Never throws.
6209
6295
  */
6210
- _emitInteractionToAnalytics(gestureType, payload) {
6211
- if (!this._captureInteractionHeatmap) return;
6212
- if (this.active) return;
6296
+ _emitInteractionToAnalytics(gestureType, payload, occurredAt, screenName) {
6297
+ if (!this._captureInteractionHeatmap) return false;
6298
+ if (this.active) return false;
6213
6299
  const adapter = this._backendTransport;
6214
- if (!adapter) return;
6215
- const windowId = adapter.analyticsSessionId ?? "";
6216
- if (windowId !== this._heatmapWindowSessionId) {
6217
- this._heatmapWindowSessionId = windowId;
6218
- this._heatmapWindowCount = 0;
6219
- }
6220
- if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
6221
- this._heatmapWindowCount++;
6300
+ if (!adapter) return false;
6222
6301
  try {
6223
6302
  this._normalizeInteractionPayload(payload);
6224
6303
  const safePayload = redactBody(payload);
@@ -6226,16 +6305,18 @@ var init_SessionManager = __esm({
6226
6305
  eventId: generateEventId(),
6227
6306
  // sessionId is (re)stamped by the analytics lane at flush time.
6228
6307
  sessionId: adapter.analyticsSessionId ?? "",
6229
- ts: Date.now(),
6308
+ ts: occurredAt,
6230
6309
  type: "USER_ACTION",
6231
6310
  subtype: `gesture:${gestureType}`,
6232
- screen: this._lastKnownScreen ?? void 0,
6311
+ screen: screenName ?? this._lastKnownScreen ?? void 0,
6233
6312
  payload: safePayload,
6234
6313
  source: "user"
6235
6314
  };
6236
6315
  adapter.trackEvent(event);
6316
+ return true;
6237
6317
  } catch (err) {
6238
6318
  logger.error("[SessionManager] heatmap analytics emit failed:", err);
6319
+ return false;
6239
6320
  }
6240
6321
  }
6241
6322
  /**
@@ -6401,13 +6482,14 @@ var init_SessionManager = __esm({
6401
6482
  * Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
6402
6483
  * Also triggers frame capture for desktop-initiated recordings.
6403
6484
  */
6404
- onUserAction(subtype, payload) {
6485
+ onUserAction(subtype, payload, context) {
6405
6486
  if (!this.active) return;
6406
6487
  this.emitEvent("USER_ACTION", {
6407
6488
  subtype,
6408
- screen: this._lastKnownScreen ?? void 0,
6489
+ screen: context?.screen ?? this._lastKnownScreen ?? void 0,
6409
6490
  payload,
6410
- source: "user"
6491
+ source: "user",
6492
+ timestamp: context?.timestamp
6411
6493
  });
6412
6494
  }
6413
6495
  /**
@@ -6415,7 +6497,8 @@ var init_SessionManager = __esm({
6415
6497
  * Emits a USER_ACTION event with gesture-specific subtype and payload.
6416
6498
  */
6417
6499
  onGestureDetected(gestureType, details) {
6418
- const dedupSig = `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
6500
+ const suppliedInteractionId = details?.interactionId;
6501
+ const dedupSig = suppliedInteractionId ? `id:${suppliedInteractionId}` : `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
6419
6502
  const nowTs = Date.now();
6420
6503
  if (dedupSig === this._lastGestureSig && nowTs - this._lastGestureTs <= _SessionManager.GESTURE_DEDUP_WINDOW_MS) {
6421
6504
  this._lastGestureTs = nowTs;
@@ -6423,7 +6506,15 @@ var init_SessionManager = __esm({
6423
6506
  }
6424
6507
  this._lastGestureSig = dedupSig;
6425
6508
  this._lastGestureTs = nowTs;
6509
+ const interactionId = suppliedInteractionId ?? generateInteractionId();
6510
+ const occurredAt = details?.occurredAt ?? Date.now();
6511
+ const stateStatus = details?.stateStatus ?? (details?.ui ? "captured_nonempty" : "not_captured");
6426
6512
  const payload = {
6513
+ interaction_id: interactionId,
6514
+ interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
6515
+ state_status: stateStatus,
6516
+ ui: details?.ui,
6517
+ target_id: details?.targetId,
6427
6518
  gestureType,
6428
6519
  x: details?.x,
6429
6520
  y: details?.y,
@@ -6461,12 +6552,23 @@ var init_SessionManager = __esm({
6461
6552
  screenHeight: details?.screenHeight,
6462
6553
  platform: details?.platform
6463
6554
  };
6464
- this._emitInteractionToAnalytics(gestureType, {
6555
+ const analyticsReplayCarrier = this._emitInteractionToAnalytics(gestureType, {
6465
6556
  ...payload
6466
- });
6557
+ }, occurredAt, details?.screenName);
6558
+ const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
6559
+ if (details?.emitAutomaticAnalytics) {
6560
+ try {
6561
+ const safePayload = redactBody(payload);
6562
+ emitAutomaticEvent("element_interacted", automaticInteractionProperties(safePayload, details?.screenName ?? this._lastKnownScreen ?? void 0, replayCarrier), occurredAt);
6563
+ } catch {
6564
+ }
6565
+ }
6467
6566
  if (!this.active) return;
6468
6567
  const target = typeof details?.target === "string" ? details.target.trim() : "";
6469
- this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload);
6568
+ this.onUserAction(target ? `gesture:${gestureType} \xB7 ${target}` : `gesture:${gestureType}`, payload, {
6569
+ screen: details?.screenName,
6570
+ timestamp: occurredAt
6571
+ });
6470
6572
  }
6471
6573
  /**
6472
6574
  * Manual frame capture — triggered by Desktop "Capture Step" button.
@@ -7781,12 +7883,12 @@ var init_AppLaunchCollector = __esm({
7781
7883
  });
7782
7884
 
7783
7885
  // lib/module/features/performance/collectors/ScreenLoadCollector.js
7784
- var MAX_PENDING, ScreenLoadCollector;
7886
+ var MAX_PENDING2, ScreenLoadCollector;
7785
7887
  var init_ScreenLoadCollector = __esm({
7786
7888
  "lib/module/features/performance/collectors/ScreenLoadCollector.js"() {
7787
7889
  "use strict";
7788
7890
  init_models();
7789
- MAX_PENDING = 20;
7891
+ MAX_PENDING2 = 20;
7790
7892
  ScreenLoadCollector = class {
7791
7893
  pending = /* @__PURE__ */ new Map();
7792
7894
  lastScreen = null;
@@ -7801,7 +7903,7 @@ var init_ScreenLoadCollector = __esm({
7801
7903
  */
7802
7904
  markStart(screenName) {
7803
7905
  if (!this.config.screenLoad) return;
7804
- if (this.pending.size >= MAX_PENDING) {
7906
+ if (this.pending.size >= MAX_PENDING2) {
7805
7907
  const oldest = this.pending.keys().next().value;
7806
7908
  if (oldest) this.pending.delete(oldest);
7807
7909
  }
@@ -10490,6 +10592,42 @@ var init_bootstrap = __esm({
10490
10592
  }
10491
10593
  });
10492
10594
 
10595
+ // lib/module/core/lifecycle/appLifecycle.js
10596
+ var appLifecycle_exports = {};
10597
+ __export(appLifecycle_exports, {
10598
+ appLifecycle: () => appLifecycle
10599
+ });
10600
+ var import_react_native13, AppLifecycle, appLifecycle;
10601
+ var init_appLifecycle = __esm({
10602
+ "lib/module/core/lifecycle/appLifecycle.js"() {
10603
+ "use strict";
10604
+ import_react_native13 = require("react-native");
10605
+ init_internalLogger();
10606
+ AppLifecycle = class {
10607
+ listeners = [];
10608
+ currentState = import_react_native13.AppState.currentState;
10609
+ constructor() {
10610
+ import_react_native13.AppState.addEventListener("change", this.handleStateChange);
10611
+ }
10612
+ handleStateChange = (nextState) => {
10613
+ __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
10614
+ this.currentState = nextState;
10615
+ this.listeners.forEach((l) => l(nextState));
10616
+ };
10617
+ addListener(listener) {
10618
+ this.listeners.push(listener);
10619
+ }
10620
+ removeListener(listener) {
10621
+ this.listeners = this.listeners.filter((l) => l !== listener);
10622
+ }
10623
+ getCurrentState() {
10624
+ return this.currentState;
10625
+ }
10626
+ };
10627
+ appLifecycle = new AppLifecycle();
10628
+ }
10629
+ });
10630
+
10493
10631
  // lib/module/features/crash/CrashReporter.js
10494
10632
  function getCrashReporter() {
10495
10633
  if (!_reporter) _reporter = new CrashReporter();
@@ -10933,58 +11071,6 @@ var init_batching = __esm({
10933
11071
  }
10934
11072
  });
10935
11073
 
10936
- // lib/module/analytics/automaticEvents.js
10937
- function compactProperties(properties) {
10938
- const out = {
10939
- capture_source: "automatic"
10940
- };
10941
- for (const [key, value] of Object.entries(properties ?? {})) {
10942
- if (value !== void 0) out[key] = value;
10943
- }
10944
- return out;
10945
- }
10946
- function emitAutomaticEvent(name, properties) {
10947
- const event = {
10948
- name,
10949
- properties: compactProperties(properties)
10950
- };
10951
- if (listeners.size === 0) {
10952
- pending3.push(event);
10953
- if (pending3.length > MAX_PENDING2) pending3.shift();
10954
- return;
10955
- }
10956
- for (const listener of listeners) {
10957
- try {
10958
- listener(event);
10959
- } catch {
10960
- }
10961
- }
10962
- }
10963
- function subscribeAutomaticEvents(listener) {
10964
- listeners.add(listener);
10965
- if (pending3.length > 0) {
10966
- const buffered = pending3.splice(0, pending3.length);
10967
- for (const event of buffered) {
10968
- try {
10969
- listener(event);
10970
- } catch {
10971
- }
10972
- }
10973
- }
10974
- return () => {
10975
- listeners.delete(listener);
10976
- };
10977
- }
10978
- var listeners, pending3, MAX_PENDING2;
10979
- var init_automaticEvents = __esm({
10980
- "lib/module/analytics/automaticEvents.js"() {
10981
- "use strict";
10982
- listeners = /* @__PURE__ */ new Set();
10983
- pending3 = [];
10984
- MAX_PENDING2 = 50;
10985
- }
10986
- });
10987
-
10988
11074
  // lib/module/analytics/EventTracker.js
10989
11075
  function uuid() {
10990
11076
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -11053,7 +11139,7 @@ var init_EventTracker = __esm({
11053
11139
  this.started = true;
11054
11140
  if (this.cfg.automaticEventTracking) {
11055
11141
  this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
11056
- this.track(event.name, event.properties);
11142
+ this.track(event.name, event.properties, event.timestamp);
11057
11143
  });
11058
11144
  }
11059
11145
  if (this.cfg.autoLifecycleEvents) {
@@ -11157,9 +11243,9 @@ var init_EventTracker = __esm({
11157
11243
  }
11158
11244
  }
11159
11245
  // ─── tracking ────────────────────────────────────────────────────────────
11160
- track(eventName, properties) {
11246
+ track(eventName, properties, timestamp) {
11161
11247
  try {
11162
- this.enqueue(this.buildEnvelope(eventName, properties));
11248
+ this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
11163
11249
  try {
11164
11250
  this.cfg.onEvent?.(eventName);
11165
11251
  } catch {
@@ -11263,7 +11349,7 @@ var init_EventTracker = __esm({
11263
11349
  };
11264
11350
  }
11265
11351
  // ─── internals ─────────────────────────────────────────────────────────────
11266
- buildEnvelope(eventName, properties) {
11352
+ buildEnvelope(eventName, properties, timestamp) {
11267
11353
  const ctx = this.cfg.context ?? {};
11268
11354
  let canonicalSessionId;
11269
11355
  try {
@@ -11274,7 +11360,7 @@ var init_EventTracker = __esm({
11274
11360
  const env = {
11275
11361
  event_id: uuid(),
11276
11362
  event_name: eventName,
11277
- event_time: Date.now(),
11363
+ event_time: timestamp ?? Date.now(),
11278
11364
  app_id: this.cfg.appId,
11279
11365
  platform: this.cfg.platform ?? resolveEventPlatform(),
11280
11366
  installation_id: this.installationId,
@@ -12126,6 +12212,11 @@ async function deliverOtaEvents(params) {
12126
12212
  kind: "ota_event",
12127
12213
  type: e.type,
12128
12214
  bundleId: e.bundleId,
12215
+ // Without this the backend stored a null releaseId on every row it ingested,
12216
+ // while serving the release id on every check — so the delivery funnel could
12217
+ // only ever be grouped by bundle, and a bundle re-promoted under a second
12218
+ // release merged the two into one indistinguishable series.
12219
+ releaseId: e.releaseId,
12129
12220
  installationId: params.installationId,
12130
12221
  // OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
12131
12222
  // resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
@@ -12227,20 +12318,31 @@ var init_OtaOrchestrator = __esm({
12227
12318
  this.bootGuardConfig = config ?? {};
12228
12319
  this.signatureConfig = config?.signature;
12229
12320
  __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12230
- this.hydrateCurrentBundleFromSlots();
12321
+ const slotState = this.readSlotState();
12322
+ this.hydrateCurrentBundleFromSlots(slotState);
12231
12323
  void prefetchDeviceCountry();
12324
+ this.checkBootGuardRecovery(slotState);
12232
12325
  this.verifyRunningBundleIdentity();
12233
- this.checkBootGuardRecovery();
12234
12326
  });
12235
12327
  }
12236
12328
  /**
12237
12329
  * Read the active slot back into `currentBundle` so the next check reports
12238
12330
  * what this device is genuinely running.
12239
12331
  */
12240
- hydrateCurrentBundleFromSlots() {
12241
- if (!NativeScaleBunOta_default) return;
12332
+ /**
12333
+ * Parse the native slot state once. Returns null when the module is absent or
12334
+ * the payload is unreadable — every caller treats that as "factory bundle".
12335
+ */
12336
+ readSlotState() {
12337
+ if (!NativeScaleBunOta_default) return null;
12338
+ try {
12339
+ return JSON.parse(NativeScaleBunOta_default.getSlotState());
12340
+ } catch {
12341
+ return null;
12342
+ }
12343
+ }
12344
+ hydrateCurrentBundleFromSlots(state) {
12242
12345
  try {
12243
- const state = JSON.parse(NativeScaleBunOta_default.getSlotState());
12244
12346
  const current = state?.current;
12245
12347
  if (!current?.sha256) return;
12246
12348
  const record4 = this.readInstallRecord();
@@ -12248,6 +12350,7 @@ var init_OtaOrchestrator = __esm({
12248
12350
  this.currentBundle = {
12249
12351
  id: record4.bundleId,
12250
12352
  version: record4.version,
12353
+ releaseId: record4.releaseId ?? void 0,
12251
12354
  sha256: record4.sha256
12252
12355
  };
12253
12356
  __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
@@ -12278,29 +12381,23 @@ var init_OtaOrchestrator = __esm({
12278
12381
  verifyRunningBundleIdentity() {
12279
12382
  if (!this.currentBundle) return;
12280
12383
  const running = readRunningBundleMarker();
12281
- const expected = this.readInstallExpectation();
12282
- if (expected && expected.bundleId !== this.currentBundle.id) {
12384
+ const record4 = this.readInstallRecord();
12385
+ if (!record4) return;
12386
+ if (record4.sha256 !== this.currentBundle.sha256) {
12283
12387
  this.clearInstallExpectation();
12284
12388
  return;
12285
12389
  }
12286
- if (expected) {
12287
- if (running === expected.identityToken) {
12288
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12289
- this.clearInstallExpectation();
12290
- return;
12291
- }
12292
- logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
12293
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12294
- error: `install_not_effective \u2014 expected ${expected.identityToken}, running ${running ?? "none"}`
12295
- });
12390
+ if (!record4.identityToken) return;
12391
+ if (running === record4.identityToken) {
12392
+ __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12393
+ this.retireIdentityToken(record4);
12296
12394
  return;
12297
12395
  }
12298
- if (running && running !== this.currentBundle.id) {
12299
- logger.error(`[OTA] BUNDLE MISMATCH \u2014 slot says ${this.currentBundle.id} is active but the running bundle identifies as ${running}.`);
12300
- otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12301
- error: `bundle_identity_mismatch \u2014 running ${running}`
12302
- });
12303
- }
12396
+ logger.error(`[OTA] INSTALL DID NOT TAKE EFFECT \u2014 bundle ${this.currentBundle.id} was installed and carries a known identity marker, but the running bundle reports ${running ?? "no marker at all"}. The app is executing different code than the slot manager believes. Check that the host app resolves the OTA bundle path at launch (see the ScaleBunOta integration for your React Native version).`);
12397
+ otaEventEmitter.emitSimple("APPLY_FAILED", this.currentBundle.id, {
12398
+ releaseId: this.currentBundle.releaseId,
12399
+ error: `install_not_effective \u2014 expected ${record4.identityToken}, running ${running ?? "none"}`
12400
+ });
12304
12401
  }
12305
12402
  // ── Install expectation ────────────────────────────────────────────────────
12306
12403
  // Written just before the restart that activates a bundle, read on the next
@@ -12312,6 +12409,11 @@ var init_OtaOrchestrator = __esm({
12312
12409
  this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
12313
12410
  bundleId: bundle.id,
12314
12411
  version: bundle.version,
12412
+ // Carried so telemetry emitted on a LATER launch (a boot-guard
12413
+ // rollback, an ineffective install) can still be attributed to the
12414
+ // release, not merely the bundle. The check response is long gone by
12415
+ // then; this record is the only thing that remembers.
12416
+ releaseId: bundle.releaseId ?? null,
12315
12417
  // The join key back to the native slot, which records sha256 and
12316
12418
  // nothing else identifying.
12317
12419
  sha256: bundle.sha256,
@@ -12330,12 +12432,26 @@ var init_OtaOrchestrator = __esm({
12330
12432
  return null;
12331
12433
  }
12332
12434
  }
12333
- readInstallExpectation() {
12334
- const record4 = this.readInstallRecord();
12335
- return record4 && record4.identityToken ? {
12336
- bundleId: record4.bundleId,
12337
- identityToken: record4.identityToken
12338
- } : null;
12435
+ /**
12436
+ * Drop the identity token once the install has been proven, keeping the rest
12437
+ * of the record.
12438
+ *
12439
+ * The record does two jobs: it proves an install took effect (once), and it
12440
+ * maps the native slot's sha256 back to a bundle id (for the life of that
12441
+ * bundle). Only the first job is finished after a successful verification, so
12442
+ * only the token is retired.
12443
+ */
12444
+ retireIdentityToken(record4) {
12445
+ try {
12446
+ this.storage().set(_OtaOrchestrator.INSTALL_EXPECTATION_KEY, JSON.stringify({
12447
+ bundleId: record4.bundleId,
12448
+ version: record4.version,
12449
+ releaseId: record4.releaseId ?? null,
12450
+ sha256: record4.sha256,
12451
+ identityToken: null
12452
+ }));
12453
+ } catch {
12454
+ }
12339
12455
  }
12340
12456
  clearInstallExpectation() {
12341
12457
  try {
@@ -12352,17 +12468,24 @@ var init_OtaOrchestrator = __esm({
12352
12468
  * If getSlotState() shows bootMarkerPresent=false but we have a 'previous' slot
12353
12469
  * and no 'current' OTA bundle, the native layer already reverted.
12354
12470
  */
12355
- checkBootGuardRecovery() {
12356
- if (!NativeScaleBunOta_default) return;
12471
+ checkBootGuardRecovery(state) {
12357
12472
  try {
12358
- const stateJson = NativeScaleBunOta_default.getSlotState();
12359
- const state = JSON.parse(stateJson);
12360
- if (state.bootGuardReverted) {
12361
- logger.warn("[OTA] Boot guard fired \u2014 app was reverted to previous bundle");
12362
- otaEventEmitter.emitSimple("AUTO_ROLLBACK", state.previous?.bundleId ?? "unknown", {
12363
- reason: "boot_guard_crash_loop_detected"
12473
+ if (!state?.bootGuardReverted) return;
12474
+ const record4 = this.readInstallRecord();
12475
+ const revertedSha = state.bootGuardRevertedSha256;
12476
+ const matchesRecord = !!record4 && (!revertedSha || record4.sha256 === revertedSha);
12477
+ const reason = state.bootGuardRevertReason || "boot_crash_guard";
12478
+ if (matchesRecord && record4) {
12479
+ logger.warn(`[OTA] Boot guard fired \u2014 reverted away from bundle ${record4.bundleId} (v${record4.version}); reason: ${reason}`);
12480
+ otaEventEmitter.emitSimple("AUTO_ROLLBACK", record4.bundleId, {
12481
+ releaseId: record4.releaseId ?? void 0,
12482
+ version: record4.version,
12483
+ reason
12364
12484
  });
12485
+ this.clearInstallExpectation();
12486
+ return;
12365
12487
  }
12488
+ logger.warn(`[OTA] Boot guard fired (reason: ${reason}) but the rolled-back bundle could not be identified locally \u2014 no install record. The rollback is not reported to the server.`);
12366
12489
  } catch {
12367
12490
  }
12368
12491
  }
@@ -12453,6 +12576,12 @@ var init_OtaOrchestrator = __esm({
12453
12576
  };
12454
12577
  }
12455
12578
  __DEV__ && logger.debug("[OTA] Sync started\u2026");
12579
+ if (this.currentBundle?.id) {
12580
+ otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
12581
+ releaseId: this.currentBundle.releaseId,
12582
+ version: this.currentBundle.version
12583
+ });
12584
+ }
12456
12585
  const checkRes = await this.checkForUpdate(params);
12457
12586
  if (checkRes.action === "NONE") {
12458
12587
  __DEV__ && logger.debug("[OTA] App is up to date");
@@ -12463,7 +12592,9 @@ var init_OtaOrchestrator = __esm({
12463
12592
  }
12464
12593
  if (checkRes.action === "ROLLBACK") {
12465
12594
  logger.warn("[OTA] Server requested ROLLBACK \u2014 reverting to previous bundle");
12466
- otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown");
12595
+ otaEventEmitter.emitSimple("MANUAL_ROLLBACK", this.currentBundle?.id ?? "unknown", {
12596
+ releaseId: this.currentBundle?.releaseId
12597
+ });
12467
12598
  const reverted = await NativeScaleBunOta_default.revertToPrevious();
12468
12599
  if (reverted) {
12469
12600
  this.currentBundle = null;
@@ -12487,9 +12618,14 @@ var init_OtaOrchestrator = __esm({
12487
12618
  }
12488
12619
  const bundle = checkRes.bundle;
12489
12620
  let patchUsed = false;
12621
+ otaEventEmitter.emitSimple("OFFERED", bundle.id, {
12622
+ releaseId: bundle.releaseId,
12623
+ version: bundle.version
12624
+ });
12490
12625
  const signatureOutcome = await verifyBundleSignature(bundle.sha256, bundle.signature, this.signatureConfig);
12491
12626
  if (!signatureOutcome.ok) {
12492
12627
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12628
+ releaseId: bundle.releaseId,
12493
12629
  error: `Signature check failed: ${signatureOutcome.reason}`,
12494
12630
  version: bundle.version
12495
12631
  });
@@ -12500,6 +12636,7 @@ var init_OtaOrchestrator = __esm({
12500
12636
  };
12501
12637
  }
12502
12638
  otaEventEmitter.emitSimple("DOWNLOAD_STARTED", bundle.id, {
12639
+ releaseId: bundle.releaseId,
12503
12640
  version: bundle.version
12504
12641
  });
12505
12642
  const downloadStart = Date.now();
@@ -12585,6 +12722,7 @@ var init_OtaOrchestrator = __esm({
12585
12722
  postProgress(0, "FAILED");
12586
12723
  logger.error("[OTA] Staging bundle failed after retries");
12587
12724
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12725
+ releaseId: bundle.releaseId,
12588
12726
  error: "Staging failed \u2014 SHA-256 mismatch or download error",
12589
12727
  version: bundle.version
12590
12728
  });
@@ -12597,6 +12735,7 @@ var init_OtaOrchestrator = __esm({
12597
12735
  postProgress(bundle.size, "COMPLETED");
12598
12736
  const downloadDuration = Date.now() - downloadStart;
12599
12737
  otaEventEmitter.emitSimple("DOWNLOAD_COMPLETE", bundle.id, {
12738
+ releaseId: bundle.releaseId,
12600
12739
  version: bundle.version,
12601
12740
  durationMs: downloadDuration,
12602
12741
  patchUsed
@@ -12605,6 +12744,7 @@ var init_OtaOrchestrator = __esm({
12605
12744
  if (!applied) {
12606
12745
  logger.error("[OTA] Applying update failed");
12607
12746
  otaEventEmitter.emitSimple("APPLY_FAILED", bundle.id, {
12747
+ releaseId: bundle.releaseId,
12608
12748
  error: "Atomic slot swap failed",
12609
12749
  version: bundle.version
12610
12750
  });
@@ -12618,6 +12758,7 @@ var init_OtaOrchestrator = __esm({
12618
12758
  this.isRestartRequiredState = true;
12619
12759
  this.recordInstallExpectation(bundle);
12620
12760
  otaEventEmitter.emitSimple("INSTALLED", bundle.id, {
12761
+ releaseId: bundle.releaseId,
12621
12762
  version: bundle.version
12622
12763
  });
12623
12764
  __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
@@ -12670,6 +12811,14 @@ var init_OtaOrchestrator = __esm({
12670
12811
  NativeScaleBunOta_default?.markHealthy();
12671
12812
  __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12672
12813
  this.healthyTimer = null;
12814
+ const running = this.currentBundle;
12815
+ if (running?.id) {
12816
+ otaEventEmitter.emitSimple("BOOT_SUCCESS", running.id, {
12817
+ releaseId: running.releaseId,
12818
+ version: running.version,
12819
+ durationMs: healthyMs
12820
+ });
12821
+ }
12673
12822
  });
12674
12823
  }, healthyMs);
12675
12824
  } catch {
@@ -12760,7 +12909,14 @@ function clearUiState(name) {
12760
12909
  }
12761
12910
  function uiStateSignature() {
12762
12911
  if (!declared.size) return void 0;
12763
- return [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`).join(";").slice(0, 96);
12912
+ const pairs = [...declared.keys()].sort().map((k) => `${k}:${declared.get(k)}`);
12913
+ let signature = "";
12914
+ for (const pair of pairs) {
12915
+ const next = signature ? `${signature};${pair}` : pair;
12916
+ if (next.length > 96) break;
12917
+ signature = next;
12918
+ }
12919
+ return signature || void 0;
12764
12920
  }
12765
12921
  var declared, clean;
12766
12922
  var init_uiState = __esm({
@@ -14984,31 +15140,8 @@ var FlushScheduler = class {
14984
15140
  }
14985
15141
  };
14986
15142
 
14987
- // lib/module/core/lifecycle/appLifecycle.js
14988
- var import_react_native13 = require("react-native");
14989
- init_internalLogger();
14990
- var AppLifecycle = class {
14991
- listeners = [];
14992
- currentState = import_react_native13.AppState.currentState;
14993
- constructor() {
14994
- import_react_native13.AppState.addEventListener("change", this.handleStateChange);
14995
- }
14996
- handleStateChange = (nextState) => {
14997
- __DEV__ && logger.debug(`App state changed: ${this.currentState} -> ${nextState}`);
14998
- this.currentState = nextState;
14999
- this.listeners.forEach((l) => l(nextState));
15000
- };
15001
- addListener(listener) {
15002
- this.listeners.push(listener);
15003
- }
15004
- removeListener(listener) {
15005
- this.listeners = this.listeners.filter((l) => l !== listener);
15006
- }
15007
- getCurrentState() {
15008
- return this.currentState;
15009
- }
15010
- };
15011
- var appLifecycle = new AppLifecycle();
15143
+ // lib/module/bootstrap/SDKBootstrapper.js
15144
+ init_appLifecycle();
15012
15145
 
15013
15146
  // lib/module/bootstrap/FeatureRegistry.js
15014
15147
  init_internalLogger();
@@ -15668,14 +15801,14 @@ function installRejectionHandler(onRejection, deps) {
15668
15801
  allRejections: true,
15669
15802
  onUnhandled: (id, error) => {
15670
15803
  const err = toError(error);
15671
- pending2.set(id, err);
15804
+ pending3.set(id, err);
15672
15805
  onRejection(err);
15673
15806
  if (isDev) {
15674
15807
  console.warn(`Possible unhandled promise rejection (id: ${id}):`, err?.message ?? err);
15675
15808
  }
15676
15809
  },
15677
15810
  onHandled: (id) => {
15678
- pending2.delete(id);
15811
+ pending3.delete(id);
15679
15812
  if (isDev) {
15680
15813
  console.warn(`Promise rejection handled late (id: ${id}) \u2014 it was already reported.`);
15681
15814
  }
@@ -15685,7 +15818,7 @@ function installRejectionHandler(onRejection, deps) {
15685
15818
  return true;
15686
15819
  }) ?? false;
15687
15820
  }
15688
- var pending2 = /* @__PURE__ */ new Map();
15821
+ var pending3 = /* @__PURE__ */ new Map();
15689
15822
 
15690
15823
  // lib/module/features/crash/CrashFeature.js
15691
15824
  init_CrashReporter();
@@ -20050,7 +20183,7 @@ async function captureInstallReferrerOnce(sink) {
20050
20183
  }
20051
20184
 
20052
20185
  // lib/module/public/ScaleBunFacade.js
20053
- var ScaleBunFacade = class {
20186
+ var ScaleBunFacade = class _ScaleBunFacade {
20054
20187
  initialized = false;
20055
20188
  /** In-flight init promise — guards against a second init() racing before the first resolves. */
20056
20189
  _initInFlight = null;
@@ -20210,6 +20343,7 @@ var ScaleBunFacade = class {
20210
20343
  logger.warn("[ScaleBun] A signing key is pinned but no signature verifier is available. Signature checking is fail-closed: updates will be REJECTED until one exists. Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further code needed), or supply `ota.verifySignature`.");
20211
20344
  }
20212
20345
  }
20346
+ const healthyAfterMs = typeof ota.healthyAfterMs === "number" ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === "number" ? ota.healthyTimeoutMs : void 0;
20213
20347
  otaOrchestrator2.init({
20214
20348
  ...signingRequested ? {
20215
20349
  signature: {
@@ -20219,15 +20353,99 @@ var ScaleBunFacade = class {
20219
20353
  verifier
20220
20354
  }
20221
20355
  } : {},
20222
- ...typeof ota.healthyTimeoutMs === "number" ? {
20223
- healthyTimeoutMs: ota.healthyTimeoutMs
20356
+ ...healthyAfterMs !== void 0 ? {
20357
+ healthyAfterMs
20224
20358
  } : {}
20225
20359
  });
20226
20360
  logger.info("[ScaleBun] OTA enabled from init config.");
20361
+ this._startOtaChecks(ota);
20227
20362
  } catch (err) {
20228
20363
  logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
20229
20364
  }
20230
20365
  }
20366
+ /** Guards against overlapping config-driven OTA checks. */
20367
+ _otaCheckInFlight = false;
20368
+ /** Wall clock of the last config-driven check, for the foreground floor. */
20369
+ _otaLastCheckAt = 0;
20370
+ _otaForegroundListener = null;
20371
+ /**
20372
+ * Minimum gap between config-driven checks.
20373
+ *
20374
+ * A foreground transition is cheap to trigger — app switchers, permission
20375
+ * dialogs and share sheets all produce one — so an unthrottled check would
20376
+ * put a request on the hot path every time the user glanced away. Ten
20377
+ * minutes is well below any realistic release cadence and well above that
20378
+ * noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
20379
+ * which is never throttled.
20380
+ */
20381
+ static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1e3;
20382
+ /**
20383
+ * Drive OTA checks from init config: once at startup, then on each
20384
+ * foreground when `checkOnForeground` is on (the schema default).
20385
+ *
20386
+ * `appVersion` is resolved from the native bridge rather than asked of the
20387
+ * integrator, because it gates the server's `targetAppVersion` semver check
20388
+ * — sending a wrong or invented value is worse than sending none, and there
20389
+ * is no honest default. If it cannot be resolved, the check is skipped with
20390
+ * a warning instead of guessing.
20391
+ */
20392
+ _startOtaChecks(ota) {
20393
+ if (__DEV__) {
20394
+ logger.info("[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).");
20395
+ return;
20396
+ }
20397
+ const runCheck = async (trigger) => {
20398
+ if (this._otaCheckInFlight) return;
20399
+ if (trigger === "foreground" && Date.now() - this._otaLastCheckAt < _ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
20400
+ return;
20401
+ }
20402
+ const clientKey = this._clientKey;
20403
+ const apiUrl = this._apiBaseUrl;
20404
+ if (!clientKey || !apiUrl) return;
20405
+ this._otaCheckInFlight = true;
20406
+ try {
20407
+ const info = await bridgeAdapter.getDeviceInfo();
20408
+ const appVersion = info?.appVersion;
20409
+ if (!appVersion) {
20410
+ logger.warn("[ScaleBun] OTA check skipped \u2014 the app version could not be read from the native bridge. Rebuild the native app, or drive checks yourself with useOtaUpdate({ appVersion }).");
20411
+ return;
20412
+ }
20413
+ this._otaLastCheckAt = Date.now();
20414
+ const {
20415
+ otaOrchestrator: otaOrchestrator2
20416
+ } = (init_OtaOrchestrator(), __toCommonJS(OtaOrchestrator_exports));
20417
+ await otaOrchestrator2.sync({
20418
+ apiUrl,
20419
+ clientKey,
20420
+ appVersion,
20421
+ // The documented option, finally connected. Omitted means the
20422
+ // server's `default` channel, exactly as before.
20423
+ channelName: typeof ota.channelOverride === "string" ? ota.channelOverride : void 0
20424
+ // Never forced from config: the release's own installMode
20425
+ // decides when the app restarts, and yanking the screen out
20426
+ // from under a user is not a decision this switch should make.
20427
+ });
20428
+ } catch (err) {
20429
+ logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
20430
+ } finally {
20431
+ this._otaCheckInFlight = false;
20432
+ }
20433
+ };
20434
+ void runCheck("startup");
20435
+ if (ota.checkOnForeground === false) return;
20436
+ if (this._otaForegroundListener) return;
20437
+ try {
20438
+ const {
20439
+ appLifecycle: appLifecycle2
20440
+ } = (init_appLifecycle(), __toCommonJS(appLifecycle_exports));
20441
+ this._otaForegroundListener = (state) => {
20442
+ if (state === "active") void runCheck("foreground");
20443
+ };
20444
+ appLifecycle2.addListener(this._otaForegroundListener);
20445
+ } catch {
20446
+ this._otaForegroundListener = null;
20447
+ }
20448
+ }
20231
20449
  _autoEnableDebug(debugConfig) {
20232
20450
  try {
20233
20451
  const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
@@ -21639,6 +21857,7 @@ init_internalLogger();
21639
21857
 
21640
21858
  // lib/module/features/engage/engageTriggerEngine.js
21641
21859
  init_internalLogger();
21860
+ init_appLifecycle();
21642
21861
  init_AutoScreenDetector();
21643
21862
  init_StorageBackend();
21644
21863
  init_engageSignals();
@@ -21946,6 +22165,7 @@ function installEngageTriggerEngine(deps) {
21946
22165
  }
21947
22166
 
21948
22167
  // lib/module/features/engage/EngagePromptProvider.js
22168
+ init_appLifecycle();
21949
22169
  init_deviceId();
21950
22170
  init_api();
21951
22171
  init_SessionManager();
@@ -33194,7 +33414,6 @@ function useEngagePrompt() {
33194
33414
  // lib/module/features/journey/ScaleBunDebugRoot.js
33195
33415
  var import_react9 = __toESM(require("react"));
33196
33416
  var import_react_native43 = require("react-native");
33197
- init_automaticEvents();
33198
33417
 
33199
33418
  // lib/module/features/journey/touchTarget.js
33200
33419
  var MAX_DEPTH = 12;
@@ -33266,6 +33485,7 @@ function describeTouchTarget(t) {
33266
33485
 
33267
33486
  // lib/module/features/journey/ScaleBunDebugRoot.js
33268
33487
  init_device();
33488
+ init_interactionProtocol();
33269
33489
  var import_jsx_runtime8 = require("react/jsx-runtime");
33270
33490
  var _bootstrap = null;
33271
33491
  function getBootstrap() {
@@ -33455,7 +33675,9 @@ function ScaleBunDebugRoot({
33455
33675
  }, [navigationRef]);
33456
33676
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
33457
33677
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
33458
- const pendingJsEmitRef = (0, import_react9.useRef)(null);
33678
+ const touchStartRef = (0, import_react9.useRef)(null);
33679
+ const interactionStartsRef = (0, import_react9.useRef)([]);
33680
+ const pendingJsEmitsRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
33459
33681
  (0, import_react9.useEffect)(() => {
33460
33682
  let subscription = null;
33461
33683
  try {
@@ -33466,9 +33688,12 @@ function ScaleBunDebugRoot({
33466
33688
  try {
33467
33689
  lastNativeTouchTsRef.current = Date.now();
33468
33690
  nativeTrackingConfirmedRef.current = true;
33469
- if (pendingJsEmitRef.current !== null) {
33470
- clearTimeout(pendingJsEmitRef.current);
33471
- pendingJsEmitRef.current = null;
33691
+ const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
33692
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
33693
+ if (start) {
33694
+ const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
33695
+ if (pending4 !== void 0) clearTimeout(pending4);
33696
+ pendingJsEmitsRef.current.delete(start.interactionId);
33472
33697
  }
33473
33698
  const {
33474
33699
  SessionManager: SessionManager2
@@ -33487,6 +33712,15 @@ function ScaleBunDebugRoot({
33487
33712
  } catch {
33488
33713
  }
33489
33714
  sm.onGestureDetected(event.gestureType || "tap", {
33715
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
33716
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
33717
+ occurredAt: nativeOccurredAt,
33718
+ ui: start?.ui,
33719
+ stateStatus: start?.stateStatus ?? "not_captured",
33720
+ target: start?.target,
33721
+ targetId: start?.targetId,
33722
+ screenName: start?.screenName,
33723
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33490
33724
  x: event.rawX,
33491
33725
  y: event.rawY,
33492
33726
  // Use the actual native end coords when present, not the down
@@ -33519,9 +33753,10 @@ function ScaleBunDebugRoot({
33519
33753
  subscription?.remove();
33520
33754
  } catch {
33521
33755
  }
33756
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
33757
+ pendingJsEmitsRef.current.clear();
33522
33758
  };
33523
- }, []);
33524
- const touchStartRef = (0, import_react9.useRef)(null);
33759
+ }, [captureAutomaticInteractions]);
33525
33760
  const rootRectRef = (0, import_react9.useRef)(null);
33526
33761
  const measureRoot = () => {
33527
33762
  try {
@@ -33553,11 +33788,39 @@ function ScaleBunDebugRoot({
33553
33788
  const handleTouchStart = (e) => {
33554
33789
  try {
33555
33790
  const touch = e.nativeEvent;
33556
- touchStartRef.current = {
33791
+ const target = resolveTouchTarget(e);
33792
+ let ui;
33793
+ let screenName;
33794
+ try {
33795
+ const {
33796
+ uiStateSignature: uiStateSignature2
33797
+ } = (init_uiState(), __toCommonJS(uiState_exports));
33798
+ ui = uiStateSignature2();
33799
+ } catch {
33800
+ }
33801
+ try {
33802
+ const {
33803
+ AutoScreenDetector: AutoScreenDetector2
33804
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33805
+ screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33806
+ } catch {
33807
+ }
33808
+ const start = {
33809
+ interactionId: generateInteractionId(),
33810
+ occurredAt: Date.now(),
33557
33811
  x: touch.pageX,
33558
33812
  y: touch.pageY,
33559
- ts: Date.now()
33813
+ locationX: touch.locationX,
33814
+ locationY: touch.locationY,
33815
+ target: describeTouchTarget(target),
33816
+ targetId: target?.testID,
33817
+ screenName,
33818
+ ui,
33819
+ stateStatus: ui ? "captured_nonempty" : "not_instrumented",
33820
+ emitAutomaticAnalytics: captureAutomaticInteractions
33560
33821
  };
33822
+ touchStartRef.current = start;
33823
+ interactionStartsRef.current = interactionStartsRef.current.filter((candidate) => start.occurredAt - candidate.occurredAt < 5e3).concat(start).slice(-8);
33561
33824
  } catch {
33562
33825
  }
33563
33826
  };
@@ -33588,21 +33851,29 @@ function ScaleBunDebugRoot({
33588
33851
  }
33589
33852
  measureRoot();
33590
33853
  const rootRect = rootRectRef.current;
33591
- const tapped = describeTouchTarget(resolveTouchTarget(e));
33854
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
33592
33855
  const gestureDetails = {
33593
33856
  target: tapped,
33594
- x: touch.pageX,
33595
- y: touch.pageY,
33596
- pageX: touch.pageX,
33597
- pageY: touch.pageY,
33598
- locationX: touch.locationX,
33599
- locationY: touch.locationY,
33857
+ interactionId: start?.interactionId ?? generateInteractionId(),
33858
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
33859
+ occurredAt: start?.occurredAt ?? Date.now(),
33860
+ ui: start?.ui,
33861
+ stateStatus: start?.stateStatus ?? "not_captured",
33862
+ targetId: start?.targetId,
33863
+ screenName: start?.screenName,
33864
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33865
+ x: start?.x ?? touch.pageX,
33866
+ y: start?.y ?? touch.pageY,
33867
+ pageX: start?.x ?? touch.pageX,
33868
+ pageY: start?.y ?? touch.pageY,
33869
+ locationX: start?.locationX ?? touch.locationX,
33870
+ locationY: start?.locationY ?? touch.locationY,
33600
33871
  viewportWidth: vpW > 0 ? vpW : void 0,
33601
33872
  viewportHeight: vpH > 0 ? vpH : void 0
33602
33873
  };
33603
33874
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
33604
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
33605
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
33875
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
33876
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
33606
33877
  gestureDetails.normalizedX = nx;
33607
33878
  gestureDetails.normalizedY = ny;
33608
33879
  gestureDetails.normalizedPrecomputed = true;
@@ -33633,7 +33904,7 @@ function ScaleBunDebugRoot({
33633
33904
  const dx = touch.pageX - start.x;
33634
33905
  const dy = touch.pageY - start.y;
33635
33906
  const dist = Math.sqrt(dx * dx + dy * dy);
33636
- const duration = Date.now() - start.ts;
33907
+ const duration = Date.now() - start.occurredAt;
33637
33908
  gestureDetails.duration = duration;
33638
33909
  if (dist >= 15) {
33639
33910
  gestureDetails.endX = touch.pageX;
@@ -33651,60 +33922,16 @@ function ScaleBunDebugRoot({
33651
33922
  gestureType = "long_press";
33652
33923
  }
33653
33924
  }
33654
- const targetInfo = _extractTarget(e);
33655
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
33656
- if (captureAutomaticInteractions) {
33657
- let screen;
33658
- try {
33659
- const {
33660
- AutoScreenDetector: AutoScreenDetector2
33661
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33662
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33663
- } catch {
33664
- }
33665
- let ui;
33666
- try {
33667
- const {
33668
- uiStateSignature: uiStateSignature2
33669
- } = (init_uiState(), __toCommonJS(uiState_exports));
33670
- ui = uiStateSignature2();
33671
- } catch {
33672
- }
33673
- emitAutomaticEvent("element_interacted", {
33674
- gesture_type: gestureType,
33675
- screen_name: screen,
33676
- /* THIS MAP ENUMERATES. A field added to the payload and forgotten here reaches the
33677
- backend as undefined with no error anywhere — the recurring defect class in this
33678
- codebase. `ui` is the key the grid aggregate reads for state, byte-identical to
33679
- the web SDK's, so one dashboard control queries both platforms. */
33680
- ui,
33681
- // testID/nativeID is an author-controlled stable identifier.
33682
- // Accessibility labels and rendered text are deliberately omitted.
33683
- target_id: targetInfo?.testId,
33684
- normalized_x: gestureDetails.normalizedX,
33685
- normalized_y: gestureDetails.normalizedY,
33686
- direction: gestureDetails.direction,
33687
- duration_ms: gestureDetails.duration
33688
- });
33689
- }
33690
33925
  if (gestureType === "tap") {
33691
33926
  try {
33692
33927
  const {
33693
33928
  gestureTriggerDetector: gestureTriggerDetector2
33694
33929
  } = (init_gestureTriggerDetector(), __toCommonJS(gestureTriggerDetector_exports));
33695
- let screen;
33696
- try {
33697
- const {
33698
- AutoScreenDetector: AutoScreenDetector2
33699
- } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33700
- screen = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
33701
- } catch {
33702
- }
33703
33930
  gestureTriggerDetector2.recordTap({
33704
- x: touch.pageX,
33705
- y: touch.pageY,
33706
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
33707
- screen
33931
+ x: start?.x ?? touch.pageX,
33932
+ y: start?.y ?? touch.pageY,
33933
+ hasTarget: !!(start?.targetId || start?.target),
33934
+ screen: start?.screenName
33708
33935
  });
33709
33936
  } catch {
33710
33937
  }
@@ -33717,11 +33944,9 @@ function ScaleBunDebugRoot({
33717
33944
  const capturedDetails = {
33718
33945
  ...gestureDetails
33719
33946
  };
33720
- if (pendingJsEmitRef.current !== null) {
33721
- clearTimeout(pendingJsEmitRef.current);
33722
- }
33723
- pendingJsEmitRef.current = setTimeout(() => {
33724
- pendingJsEmitRef.current = null;
33947
+ const interactionId = capturedDetails.interactionId;
33948
+ const timer = setTimeout(() => {
33949
+ pendingJsEmitsRef.current.delete(interactionId);
33725
33950
  if (nativeTrackingConfirmedRef.current) return;
33726
33951
  try {
33727
33952
  const {
@@ -33738,6 +33963,7 @@ function ScaleBunDebugRoot({
33738
33963
  } catch {
33739
33964
  }
33740
33965
  }, 600);
33966
+ pendingJsEmitsRef.current.set(interactionId, timer);
33741
33967
  }
33742
33968
  touchStartRef.current = null;
33743
33969
  } catch {
@@ -33813,20 +34039,6 @@ function useScaleBunScreen(name) {
33813
34039
  }
33814
34040
  }, [name, manager]);
33815
34041
  }
33816
- function _extractTarget(e) {
33817
- try {
33818
- const target = e?.target;
33819
- if (!target) return void 0;
33820
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
33821
- return {
33822
- testId: props.testID || props.nativeID || void 0,
33823
- accessibilityLabel: props.accessibilityLabel || void 0,
33824
- text: void 0
33825
- };
33826
- } catch {
33827
- return void 0;
33828
- }
33829
- }
33830
34042
  var styles4 = {
33831
34043
  root: {
33832
34044
  flex: 1
@@ -34187,7 +34399,7 @@ function useOtaUpdate(options) {
34187
34399
  setIsSyncing(true);
34188
34400
  setDownloadProgress(0);
34189
34401
  try {
34190
- const result = await otaOrchestrator.sync(options);
34402
+ const result = await otaOrchestrator.sync(optionsRef.current);
34191
34403
  setSyncResult(result);
34192
34404
  if (result.isMandatory && result.status === "UPDATE_INSTALLED" && optionsRef.current.mandatoryBlocksUi) {
34193
34405
  setMandatoryUpdatePending(true);
@@ -34196,7 +34408,7 @@ function useOtaUpdate(options) {
34196
34408
  } finally {
34197
34409
  setIsSyncing(false);
34198
34410
  }
34199
- }, [options.apiUrl, options.clientKey, options.appVersion, options.installationId, options.autoRestart]);
34411
+ }, []);
34200
34412
  const restart = (0, import_react16.useCallback)(() => {
34201
34413
  setMandatoryUpdatePending(false);
34202
34414
  otaOrchestrator.restart();