@scalebun/react-native 2.0.0 → 2.0.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 (70) hide show
  1. package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
  2. package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +18 -3
  4. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +62 -1
  5. package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
  6. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +2 -0
  7. package/dist/scalebun.full.js +840 -114
  8. package/dist/scalebun.slim.js +838 -112
  9. package/ios/ReplaySdk.swift +65 -2
  10. package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
  11. package/lib/commonjs/core/config/schema.js +16 -0
  12. package/lib/commonjs/core/constants/version.js +1 -1
  13. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
  14. package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
  15. package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
  16. package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
  17. package/lib/commonjs/features/journey/scrollContext.js +146 -0
  18. package/lib/commonjs/features/journey/targetGeometry.js +164 -0
  19. package/lib/commonjs/features/journey/touchTarget.js +152 -24
  20. package/lib/commonjs/features/journey/uiState.js +81 -1
  21. package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
  22. package/lib/commonjs/features/ota/OtaOrchestrator.js +69 -22
  23. package/lib/commonjs/features/session/SessionManager.js +63 -0
  24. package/lib/commonjs/public/ScaleBunFacade.js +18 -16
  25. package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
  26. package/lib/module/core/config/schema.js +16 -0
  27. package/lib/module/core/constants/version.js +1 -1
  28. package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
  29. package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
  30. package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
  31. package/lib/module/features/journey/interactionProtocol.js +128 -8
  32. package/lib/module/features/journey/scrollContext.js +135 -0
  33. package/lib/module/features/journey/targetGeometry.js +155 -0
  34. package/lib/module/features/journey/touchTarget.js +151 -24
  35. package/lib/module/features/journey/uiState.js +78 -1
  36. package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
  37. package/lib/module/features/ota/OtaOrchestrator.js +69 -22
  38. package/lib/module/features/session/SessionManager.js +63 -0
  39. package/lib/module/public/ScaleBunFacade.js +18 -16
  40. package/lib/typescript/core/config/schema.d.ts +2 -0
  41. package/lib/typescript/core/constants/version.d.ts +1 -1
  42. package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
  43. package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
  44. package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
  45. package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
  46. package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
  47. package/lib/typescript/features/journey/uiState.d.ts +39 -0
  48. package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
  49. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +7 -0
  50. package/lib/typescript/features/session/SessionManager.d.ts +55 -0
  51. package/lib/typescript/public/types.d.ts +10 -0
  52. package/lib/typescript/specs/NativeScaleBunOta.d.ts +2 -0
  53. package/package.json +2 -2
  54. package/src/bootstrap/SDKBootstrapper.ts +40 -0
  55. package/src/core/config/schema.ts +18 -0
  56. package/src/core/constants/version.ts +1 -1
  57. package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
  58. package/src/features/journey/ScaleBunScrollView.tsx +29 -0
  59. package/src/features/journey/autoInstrumentScroll.ts +155 -0
  60. package/src/features/journey/interactionProtocol.ts +188 -10
  61. package/src/features/journey/scrollContext.ts +137 -0
  62. package/src/features/journey/targetGeometry.ts +176 -0
  63. package/src/features/journey/touchTarget.ts +237 -26
  64. package/src/features/journey/uiState.ts +80 -1
  65. package/src/features/navigation/AutoScreenDetector.ts +65 -5
  66. package/src/features/ota/OtaOrchestrator.ts +76 -22
  67. package/src/features/session/SessionManager.ts +113 -0
  68. package/src/public/ScaleBunFacade.ts +18 -16
  69. package/src/public/types.ts +10 -0
  70. package/src/specs/NativeScaleBunOta.ts +3 -0
@@ -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 = "2.0.0";
608
+ SDK_VERSION = "2.0.1";
609
609
  }
610
610
  });
611
611
 
@@ -3461,6 +3461,15 @@ var init_AutoScreenDetector = __esm({
3461
3461
  _manualScreen = null;
3462
3462
  _manualScreenTs = 0;
3463
3463
  _autoScreen = null;
3464
+ /**
3465
+ * When `_autoScreen` was last written.
3466
+ *
3467
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
3468
+ * Without a timestamp the only comparison available was "is the manual value still
3469
+ * fresh", which is a different question and gave the wrong answer — see the note on
3470
+ * `_currentScreen()`.
3471
+ */
3472
+ _autoScreenTs = 0;
3464
3473
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
3465
3474
  _autoVisitSig = null;
3466
3475
  _navigationRef = null;
@@ -3528,6 +3537,7 @@ var init_AutoScreenDetector = __esm({
3528
3537
  if (this.isManualScreenActive()) return;
3529
3538
  if (name !== this._autoScreen) {
3530
3539
  this._autoScreen = name;
3540
+ this._autoScreenTs = Date.now();
3531
3541
  this._autoVisitSig = name;
3532
3542
  this._emitScreenChange(name, "heuristic");
3533
3543
  }
@@ -3535,8 +3545,19 @@ var init_AutoScreenDetector = __esm({
3535
3545
  }
3536
3546
  }
3537
3547
  /**
3538
- * Check if a manual screen is currently active.
3539
- * Used by auto-detection to yield to manual overrides.
3548
+ * Should a WRITER defer to the manual screen right now?
3549
+ *
3550
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
3551
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
3552
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
3553
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
3554
+ *
3555
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
3556
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
3557
+ * `setManualScreen` in production, so the effect was latent — but the moment the
3558
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
3559
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
3560
+ * fix that introduced that would have been worse than the gap it closed.
3540
3561
  */
3541
3562
  isManualScreenActive() {
3542
3563
  if (!this._manualScreen) return false;
@@ -3582,6 +3603,7 @@ var init_AutoScreenDetector = __esm({
3582
3603
  const visit = this._extractActiveVisit(state);
3583
3604
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
3584
3605
  this._autoScreen = visit.name;
3606
+ this._autoScreenTs = Date.now();
3585
3607
  this._autoVisitSig = this._visitSig(visit);
3586
3608
  this._emitScreenChange(visit.name, "navRef");
3587
3609
  }
@@ -3600,6 +3622,7 @@ var init_AutoScreenDetector = __esm({
3600
3622
  const visit = this._extractActiveVisit(state);
3601
3623
  if (visit) {
3602
3624
  this._autoScreen = visit.name;
3625
+ this._autoScreenTs = Date.now();
3603
3626
  this._autoVisitSig = this._visitSig(visit);
3604
3627
  this._emitScreenChange(visit.name, "navRef");
3605
3628
  }
@@ -3630,6 +3653,7 @@ var init_AutoScreenDetector = __esm({
3630
3653
  const visit = this._extractActiveVisit(state);
3631
3654
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
3632
3655
  this._autoScreen = visit.name;
3656
+ this._autoScreenTs = Date.now();
3633
3657
  this._autoVisitSig = this._visitSig(visit);
3634
3658
  this._emitScreenChange(visit.name, "heuristic");
3635
3659
  }
@@ -3728,7 +3752,7 @@ var init_AutoScreenDetector = __esm({
3728
3752
  if (!reactNav || reactNav.__scalebunAutoHooked) return;
3729
3753
  const detector = this;
3730
3754
  let installed2 = 0;
3731
- const redefine = (key, value) => {
3755
+ const redefine2 = (key, value) => {
3732
3756
  try {
3733
3757
  Object.defineProperty(reactNav, key, {
3734
3758
  value,
@@ -3778,7 +3802,7 @@ var init_AutoScreenDetector = __esm({
3778
3802
  });
3779
3803
  });
3780
3804
  Wrapped.displayName = "ScaleBunNavigationContainer";
3781
- if (redefine("NavigationContainer", Wrapped)) installed2++;
3805
+ if (redefine2("NavigationContainer", Wrapped)) installed2++;
3782
3806
  }
3783
3807
  if (typeof reactNav.createNavigationContainerRef === "function") {
3784
3808
  const originalCreate = reactNav.createNavigationContainerRef;
@@ -3790,7 +3814,7 @@ var init_AutoScreenDetector = __esm({
3790
3814
  }
3791
3815
  return ref;
3792
3816
  };
3793
- if (redefine("createNavigationContainerRef", patchedCreate)) installed2++;
3817
+ if (redefine2("createNavigationContainerRef", patchedCreate)) installed2++;
3794
3818
  }
3795
3819
  if (typeof reactNav.useNavigationContainerRef === "function") {
3796
3820
  const originalHook = reactNav.useNavigationContainerRef;
@@ -3802,7 +3826,7 @@ var init_AutoScreenDetector = __esm({
3802
3826
  }
3803
3827
  return ref;
3804
3828
  };
3805
- if (redefine("useNavigationContainerRef", patchedHook)) installed2++;
3829
+ if (redefine2("useNavigationContainerRef", patchedHook)) installed2++;
3806
3830
  }
3807
3831
  this._autoHookInstalled = installed2 > 0;
3808
3832
  try {
@@ -3851,15 +3875,58 @@ var init_AutoScreenDetector = __esm({
3851
3875
  this._listeners = [];
3852
3876
  this._manualScreen = null;
3853
3877
  this._autoScreen = null;
3878
+ this._autoScreenTs = 0;
3854
3879
  this._navigationRef = null;
3855
3880
  _instance = null;
3856
3881
  }
3857
3882
  /** Get the current best-known screen name */
3858
3883
  getCurrentScreen() {
3859
- if (this._manualScreen && this.isManualScreenActive()) {
3860
- return this._manualScreen;
3884
+ return this._currentScreen().name;
3885
+ }
3886
+ /**
3887
+ * WHERE the current screen name came from, or 'none'.
3888
+ *
3889
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
3890
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
3891
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
3892
+ * causes: the app never instrumented anything, the app instrumented manually and the
3893
+ * value never reached interactions, or auto-detection ran and failed. Those have
3894
+ * different owners and different fixes, and guessing between them is how a P0 stays
3895
+ * open.
3896
+ */
3897
+ getCurrentScreenSource() {
3898
+ return this._currentScreen().source;
3899
+ }
3900
+ /**
3901
+ * LAST WRITER WINS between the manual beacon and auto-detection.
3902
+ *
3903
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
3904
+ * declares a different one — that is what mounting a screen means. It does not
3905
+ * expire, which is the correction here: the previous rule returned the manual name
3906
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
3907
+ * which in a manually-instrumented app is `null`.
3908
+ *
3909
+ * Comparing timestamps rather than hard-coding a priority also handles the real
3910
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
3911
+ * beacon on one modal, and whichever spoke last is the truth.
3912
+ */
3913
+ _currentScreen() {
3914
+ const manual = this._manualScreen;
3915
+ const auto = this._autoScreen;
3916
+ if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
3917
+ return {
3918
+ name: manual,
3919
+ source: "manual"
3920
+ };
3861
3921
  }
3862
- return this._autoScreen;
3922
+ if (auto) return {
3923
+ name: auto,
3924
+ source: "navigation"
3925
+ };
3926
+ return {
3927
+ name: null,
3928
+ source: "none"
3929
+ };
3863
3930
  }
3864
3931
  // ─── Internal ───────────────────────────────────────────────────────
3865
3932
  _emitScreenChange(name, source) {
@@ -5740,17 +5807,54 @@ var init_automaticEvents = __esm({
5740
5807
  function generateInteractionId() {
5741
5808
  return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
5742
5809
  }
5743
- function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
5744
- let best;
5745
- let bestDelta = toleranceMs + 1;
5746
- for (const start of starts) {
5810
+ function correlateInteraction(starts, occurredAt, toleranceMs = CORRELATION_TOLERANCE_MS, marginMs = CORRELATION_MARGIN_MS) {
5811
+ const candidates = [];
5812
+ for (let i = 0; i < starts.length; i++) {
5813
+ const start = starts[i];
5814
+ if (!start) continue;
5747
5815
  const delta = Math.abs(start.occurredAt - occurredAt);
5748
- if (delta < bestDelta) {
5749
- best = start;
5750
- bestDelta = delta;
5816
+ if (delta <= toleranceMs) candidates.push({
5817
+ index: i,
5818
+ delta
5819
+ });
5820
+ }
5821
+ if (candidates.length === 0) {
5822
+ return {
5823
+ index: -1,
5824
+ outcome: "none",
5825
+ deltaMs: null,
5826
+ runnerUpDeltaMs: null
5827
+ };
5828
+ }
5829
+ candidates.sort((a, b) => a.delta - b.delta || a.index - b.index);
5830
+ const best = candidates[0];
5831
+ const runnerUp = candidates[1];
5832
+ if (runnerUp && runnerUp.delta - best.delta < marginMs) {
5833
+ return {
5834
+ index: -1,
5835
+ outcome: "ambiguous",
5836
+ deltaMs: best.delta,
5837
+ runnerUpDeltaMs: runnerUp.delta
5838
+ };
5839
+ }
5840
+ return {
5841
+ start: starts[best.index],
5842
+ index: best.index,
5843
+ outcome: "exact",
5844
+ deltaMs: best.delta,
5845
+ runnerUpDeltaMs: runnerUp ? runnerUp.delta : null
5846
+ };
5847
+ }
5848
+ function pruneInteractionStarts(starts, now2, toleranceMs = CORRELATION_TOLERANCE_MS) {
5849
+ let removed = 0;
5850
+ for (let i = starts.length - 1; i >= 0; i--) {
5851
+ const start = starts[i];
5852
+ if (!start || now2 - start.occurredAt > toleranceMs) {
5853
+ starts.splice(i, 1);
5854
+ removed++;
5751
5855
  }
5752
5856
  }
5753
- return bestDelta <= toleranceMs ? best : void 0;
5857
+ return removed;
5754
5858
  }
5755
5859
  function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5756
5860
  return {
@@ -5768,11 +5872,13 @@ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
5768
5872
  canonical_mirror: canonicalMirror || void 0
5769
5873
  };
5770
5874
  }
5771
- var INTERACTION_PROTOCOL_VERSION;
5875
+ var INTERACTION_PROTOCOL_VERSION, CORRELATION_TOLERANCE_MS, CORRELATION_MARGIN_MS;
5772
5876
  var init_interactionProtocol = __esm({
5773
5877
  "lib/module/features/journey/interactionProtocol.js"() {
5774
5878
  "use strict";
5775
5879
  INTERACTION_PROTOCOL_VERSION = 1;
5880
+ CORRELATION_TOLERANCE_MS = 1500;
5881
+ CORRELATION_MARGIN_MS = 100;
5776
5882
  }
5777
5883
  });
5778
5884
 
@@ -6515,6 +6621,69 @@ var init_SessionManager = __esm({
6515
6621
  state_status: stateStatus,
6516
6622
  ui: details?.ui,
6517
6623
  target_id: details?.targetId,
6624
+ /**
6625
+ * RN-6 — shipped with the row so the join is auditable instead of assumed.
6626
+ *
6627
+ * Without it, an interaction with no `target_id` is ambiguous in the worst way:
6628
+ * it could mean the control genuinely had no identity, or that the correlation
6629
+ * refused and the identity exists but could not be attributed. Those are
6630
+ * different defects with different owners, and the accounting lane cannot give
6631
+ * an honest unplaceable REASON without knowing which.
6632
+ *
6633
+ * Undefined on the JS-fallback path (no native event, so nothing was joined),
6634
+ * which is itself the correct answer there rather than a fabricated 'exact'.
6635
+ */
6636
+ correlation: details?.correlation,
6637
+ correlation_delta_ms: details?.correlationDeltaMs,
6638
+ correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
6639
+ /**
6640
+ * RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
6641
+ * identifier from a structural inference. 'path' means the app authored no testID
6642
+ * and the identity was derived from component names, which is useful but will
6643
+ * change under refactoring; a dashboard that presents the two identically invites
6644
+ * someone to trust a key that is about to move.
6645
+ */
6646
+ target_source: details?.targetSource,
6647
+ /**
6648
+ * RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
6649
+ * 'none' means the app instrumented nothing the SDK could see; 'manual' means a
6650
+ * <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
6651
+ * different owners behind one number that today has none.
6652
+ */
6653
+ screen_source: details?.screenSource,
6654
+ /**
6655
+ * RN-13 — what makes "the heat is on this control" checkable instead of assumed.
6656
+ * Absent when the control could not be measured synchronously, which is a fact worth
6657
+ * storing: an unmeasurable control and a mis-placed tap are different findings.
6658
+ */
6659
+ target_rect: details?.targetRect,
6660
+ /**
6661
+ * RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
6662
+ * table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
6663
+ * these need no migration. Mobile simply never sent them.
6664
+ *
6665
+ * `scroll_source` says whether an offset was available at all: 'none' means the app
6666
+ * uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
6667
+ * the integration and not a tap at the top of a list.
6668
+ */
6669
+ scrollX: details?.scrollX,
6670
+ scrollY: details?.scrollY,
6671
+ scroll_source: details?.scrollSource,
6672
+ /**
6673
+ * RN-4 — what stops a recycled native view merging two different rows into one
6674
+ * hotspot. The key is the app's own keyExtractor output, so it is stable across
6675
+ * scrolling and across sessions; the index is carried so a recycled slot (same index,
6676
+ * different key) can be proven rather than inferred.
6677
+ */
6678
+ item_key: details?.itemKey,
6679
+ item_index: details?.itemIndex,
6680
+ /**
6681
+ * RN-15 — where in the control, not just which control. Turns "they tapped Add to
6682
+ * Cart" into "they tapped its left edge", which is what tells you whether a hit area
6683
+ * is right. Outside 0..1 is a hitSlop tap and is kept as such.
6684
+ */
6685
+ local_u: details?.localU,
6686
+ local_v: details?.localV,
6518
6687
  gestureType,
6519
6688
  x: details?.x,
6520
6689
  y: details?.y,
@@ -8553,8 +8722,8 @@ var init_PerformanceTransport = __esm({
8553
8722
  setSink(sink) {
8554
8723
  this.sink = sink;
8555
8724
  if (sink && this.buffer.length > 0) {
8556
- const pending4 = this.buffer.splice(0);
8557
- for (const msg of pending4) {
8725
+ const pending5 = this.buffer.splice(0);
8726
+ for (const msg of pending5) {
8558
8727
  try {
8559
8728
  sink.send(msg);
8560
8729
  } catch {
@@ -9327,8 +9496,8 @@ var init_ProfilerTransport = __esm({
9327
9496
  setSink(sink) {
9328
9497
  this.sink = sink;
9329
9498
  if (sink && this.buffer.length > 0) {
9330
- const pending4 = this.buffer.splice(0);
9331
- for (const msg of pending4) {
9499
+ const pending5 = this.buffer.splice(0);
9500
+ for (const msg of pending5) {
9332
9501
  try {
9333
9502
  sink.send(msg);
9334
9503
  } catch {
@@ -10216,10 +10385,10 @@ function requestExport(transport, enabled, presetOrOptions) {
10216
10385
  return;
10217
10386
  }
10218
10387
  const timer = setTimeout(() => {
10219
- const pending4 = _pendingExports.get(requestId);
10220
- if (pending4) {
10388
+ const pending5 = _pendingExports.get(requestId);
10389
+ if (pending5) {
10221
10390
  _pendingExports.delete(requestId);
10222
- pending4.resolve({
10391
+ pending5.resolve({
10223
10392
  success: false,
10224
10393
  error: `Export timed out after ${EXPORT_TIMEOUT_MS / 1e3}s`
10225
10394
  });
@@ -10261,14 +10430,14 @@ function handleExportResult(args) {
10261
10430
  try {
10262
10431
  const requestId = args.requestId;
10263
10432
  if (!requestId) return;
10264
- const pending4 = _pendingExports.get(requestId);
10265
- if (!pending4) {
10433
+ const pending5 = _pendingExports.get(requestId);
10434
+ if (!pending5) {
10266
10435
  logger.warn(`[ExportBridge] Received result for unknown requestId: ${requestId}`);
10267
10436
  return;
10268
10437
  }
10269
- clearTimeout(pending4.timer);
10438
+ clearTimeout(pending5.timer);
10270
10439
  _pendingExports.delete(requestId);
10271
- pending4.resolve({
10440
+ pending5.resolve({
10272
10441
  success: args.success ?? false,
10273
10442
  filePath: args.filePath,
10274
10443
  error: args.error,
@@ -10279,9 +10448,9 @@ function handleExportResult(args) {
10279
10448
  }
10280
10449
  }
10281
10450
  function teardownExports() {
10282
- for (const [, pending4] of _pendingExports) {
10283
- clearTimeout(pending4.timer);
10284
- pending4.reject(new Error("Debug disabled \u2014 export cancelled"));
10451
+ for (const [, pending5] of _pendingExports) {
10452
+ clearTimeout(pending5.timer);
10453
+ pending5.reject(new Error("Debug disabled \u2014 export cancelled"));
10285
10454
  }
10286
10455
  _pendingExports.clear();
10287
10456
  }
@@ -11489,6 +11658,174 @@ var init_installationId = __esm({
11489
11658
  }
11490
11659
  });
11491
11660
 
11661
+ // lib/module/features/journey/scrollContext.js
11662
+ var scrollContext_exports = {};
11663
+ __export(scrollContext_exports, {
11664
+ SCROLL_OFFSET_TTL_MS: () => SCROLL_OFFSET_TTL_MS,
11665
+ clearScrollOffsets: () => clearScrollOffsets,
11666
+ nativeTagOf: () => nativeTagOf,
11667
+ nearestRecordedOffset: () => nearestRecordedOffset,
11668
+ recordScrollOffset: () => recordScrollOffset,
11669
+ trackedScrollerCount: () => trackedScrollerCount
11670
+ });
11671
+ function nativeTagOf(node) {
11672
+ if (!node || typeof node !== "object") return null;
11673
+ const n = node;
11674
+ const tag = typeof n.__nativeTag === "number" ? n.__nativeTag : n._nativeTag;
11675
+ return typeof tag === "number" && Number.isFinite(tag) ? tag : null;
11676
+ }
11677
+ function recordScrollOffset(tag, x, y, now2 = Date.now()) {
11678
+ if (tag === null || !Number.isFinite(x) || !Number.isFinite(y)) return;
11679
+ offsets.set(tag, {
11680
+ x,
11681
+ y,
11682
+ at: now2
11683
+ });
11684
+ if (offsets.size > MAX_TRACKED_SCROLLERS) {
11685
+ let oldestTag = null;
11686
+ let oldestAt = Infinity;
11687
+ for (const [k, v] of offsets) {
11688
+ if (v.at < oldestAt) {
11689
+ oldestAt = v.at;
11690
+ oldestTag = k;
11691
+ }
11692
+ }
11693
+ if (oldestTag !== null) offsets.delete(oldestTag);
11694
+ }
11695
+ }
11696
+ function nearestRecordedOffset(ancestorTags, now2 = Date.now()) {
11697
+ for (const tag of ancestorTags) {
11698
+ if (tag === null) continue;
11699
+ const hit = offsets.get(tag);
11700
+ if (!hit) continue;
11701
+ if (now2 - hit.at > SCROLL_OFFSET_TTL_MS) {
11702
+ offsets.delete(tag);
11703
+ continue;
11704
+ }
11705
+ return {
11706
+ x: hit.x,
11707
+ y: hit.y,
11708
+ tag
11709
+ };
11710
+ }
11711
+ return null;
11712
+ }
11713
+ function clearScrollOffsets() {
11714
+ offsets.clear();
11715
+ }
11716
+ function trackedScrollerCount() {
11717
+ return offsets.size;
11718
+ }
11719
+ var SCROLL_OFFSET_TTL_MS, MAX_TRACKED_SCROLLERS, offsets;
11720
+ var init_scrollContext = __esm({
11721
+ "lib/module/features/journey/scrollContext.js"() {
11722
+ "use strict";
11723
+ SCROLL_OFFSET_TTL_MS = 5 * 6e4;
11724
+ MAX_TRACKED_SCROLLERS = 32;
11725
+ offsets = /* @__PURE__ */ new Map();
11726
+ }
11727
+ });
11728
+
11729
+ // lib/module/features/journey/autoInstrumentScroll.js
11730
+ var autoInstrumentScroll_exports = {};
11731
+ __export(autoInstrumentScroll_exports, {
11732
+ autoInstrumentScrollViews: () => autoInstrumentScrollViews,
11733
+ resetAutoInstrumentForTests: () => resetAutoInstrumentForTests
11734
+ });
11735
+ function redefine(target, key, value) {
11736
+ try {
11737
+ Object.defineProperty(target, key, {
11738
+ configurable: true,
11739
+ enumerable: true,
11740
+ writable: true,
11741
+ value
11742
+ });
11743
+ return target[key] === value;
11744
+ } catch {
11745
+ return false;
11746
+ }
11747
+ }
11748
+ function instrument(Original, React14) {
11749
+ const Wrapped = React14.forwardRef((props, ref) => {
11750
+ const onScroll = (event) => {
11751
+ try {
11752
+ const target = event?.nativeEvent?.target;
11753
+ const offset = event?.nativeEvent?.contentOffset;
11754
+ if (offset) {
11755
+ recordScrollOffset(typeof target === "number" ? target : nativeTagOf(target), offset.x, offset.y);
11756
+ }
11757
+ } catch {
11758
+ }
11759
+ try {
11760
+ props?.onScroll?.(event);
11761
+ } catch (e) {
11762
+ setTimeout(() => {
11763
+ throw e;
11764
+ }, 0);
11765
+ }
11766
+ };
11767
+ const throttle = props?.scrollEventThrottle ?? 16;
11768
+ return React14.createElement(Original, {
11769
+ ...props,
11770
+ scrollEventThrottle: throttle,
11771
+ onScroll,
11772
+ ref
11773
+ });
11774
+ });
11775
+ Wrapped.displayName = "ScaleBunAutoScrollView";
11776
+ return Wrapped;
11777
+ }
11778
+ function autoInstrumentScrollViews() {
11779
+ if (alreadyPatched) return {
11780
+ installed: true
11781
+ };
11782
+ try {
11783
+ const RN = require("react-native");
11784
+ const React14 = require("react");
11785
+ const Original = RN.ScrollView;
11786
+ if (typeof Original !== "function" && typeof Original !== "object") {
11787
+ return {
11788
+ installed: false,
11789
+ reason: "react-native has no ScrollView export to wrap"
11790
+ };
11791
+ }
11792
+ if (Original.__scalebunInstrumented) {
11793
+ alreadyPatched = true;
11794
+ return {
11795
+ installed: true
11796
+ };
11797
+ }
11798
+ const Wrapped = instrument(Original, React14);
11799
+ Wrapped.__scalebunInstrumented = true;
11800
+ if (!redefine(RN, "ScrollView", Wrapped)) {
11801
+ return {
11802
+ installed: false,
11803
+ reason: "react-native's ScrollView export could not be replaced \u2014 it is a getter with no configurable descriptor on this bundler. Use ScaleBunScrollView explicitly instead."
11804
+ };
11805
+ }
11806
+ alreadyPatched = true;
11807
+ return {
11808
+ installed: true
11809
+ };
11810
+ } catch (e) {
11811
+ return {
11812
+ installed: false,
11813
+ reason: e?.message ?? "unknown failure"
11814
+ };
11815
+ }
11816
+ }
11817
+ function resetAutoInstrumentForTests() {
11818
+ alreadyPatched = false;
11819
+ }
11820
+ var alreadyPatched;
11821
+ var init_autoInstrumentScroll = __esm({
11822
+ "lib/module/features/journey/autoInstrumentScroll.js"() {
11823
+ "use strict";
11824
+ init_scrollContext();
11825
+ alreadyPatched = false;
11826
+ }
11827
+ });
11828
+
11492
11829
  // lib/module/features/engage/engageSignals.js
11493
11830
  var EngageSignalBus, engageSignals;
11494
11831
  var init_engageSignals = __esm({
@@ -11924,10 +12261,10 @@ async function deliverOtaEvents(params) {
11924
12261
  })
11925
12262
  });
11926
12263
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
11927
- __DEV__ && logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
12264
+ logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
11928
12265
  } catch (err) {
11929
12266
  for (const e of events) otaEventEmitter.emit(e);
11930
- __DEV__ && logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
12267
+ logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
11931
12268
  }
11932
12269
  }
11933
12270
  var import_react_native34, PROGRESS_POST_INTERVAL_MS, DEFAULT_HEALTHY_AFTER_MS, OtaOrchestrator, otaOrchestrator;
@@ -11960,6 +12297,8 @@ var init_OtaOrchestrator = __esm({
11960
12297
  return this.currentBundle;
11961
12298
  }
11962
12299
  isRestartRequiredState = false;
12300
+ /** Root/jailbreak indicators observed at init; empty when none found. */
12301
+ integrityIndicators = [];
11963
12302
  healthyTimer = null;
11964
12303
  bootGuardConfig = {};
11965
12304
  environment = null;
@@ -11994,17 +12333,27 @@ var init_OtaOrchestrator = __esm({
11994
12333
  logger.warn("[OTA] Running bridgeless but the React Native version could not be determined. If this app is below RN 0.76.1, OTA updates will install and never load.");
11995
12334
  }
11996
12335
  if (this.environment.requiresReactHostIntegration) {
11997
- __DEV__ && logger.debug("[OTA] React Native >= 0.82 detected. The only valid Android integration is getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) \u2014 ReactNativeHost and its getJSBundleFile() override no longer exist.");
12336
+ logger.debug("[OTA] React Native >= 0.82 detected. The only valid Android integration is getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) \u2014 ReactNativeHost and its getJSBundleFile() override no longer exist.");
11998
12337
  }
11999
12338
  this.initialized = true;
12000
12339
  this.enabled = true;
12340
+ try {
12341
+ const raw = NativeScaleBunOta_default?.getDeviceIntegrity?.();
12342
+ this.integrityIndicators = raw ? JSON.parse(raw) : [];
12343
+ if (this.integrityIndicators.length > 0) {
12344
+ logger.warn(`[OTA] Device integrity indicators present: ${this.integrityIndicators.join(", ")}. Updates still install and are still signature-verified, but an installed bundle on this device can be modified by other privileged processes.`);
12345
+ }
12346
+ } catch {
12347
+ this.integrityIndicators = [];
12348
+ }
12001
12349
  this.bootGuardConfig = config ?? {};
12002
- __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12350
+ logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
12003
12351
  const slotState = this.readSlotState();
12004
12352
  this.hydrateCurrentBundleFromSlots(slotState);
12005
12353
  void prefetchDeviceCountry();
12006
12354
  this.checkBootGuardRecovery(slotState);
12007
12355
  this.verifyRunningBundleIdentity();
12356
+ this.startBootGuardHeartbeat(slotState);
12008
12357
  });
12009
12358
  }
12010
12359
  /**
@@ -12035,13 +12384,13 @@ var init_OtaOrchestrator = __esm({
12035
12384
  releaseId: record4.releaseId ?? void 0,
12036
12385
  sha256: record4.sha256
12037
12386
  };
12038
- __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
12387
+ logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
12039
12388
  return;
12040
12389
  }
12041
12390
  this.currentBundle = {
12042
12391
  sha256: current.sha256
12043
12392
  };
12044
- __DEV__ && logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
12393
+ logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
12045
12394
  } catch {
12046
12395
  }
12047
12396
  }
@@ -12071,7 +12420,7 @@ var init_OtaOrchestrator = __esm({
12071
12420
  }
12072
12421
  if (!record4.identityToken) return;
12073
12422
  if (running === record4.identityToken) {
12074
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12423
+ logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
12075
12424
  this.retireIdentityToken(record4);
12076
12425
  return;
12077
12426
  }
@@ -12174,6 +12523,13 @@ var init_OtaOrchestrator = __esm({
12174
12523
  isEnabled() {
12175
12524
  return this.enabled;
12176
12525
  }
12526
+ /**
12527
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
12528
+ * Empty means nothing was detected — not proof the device is clean.
12529
+ */
12530
+ deviceIntegrityIndicators() {
12531
+ return [...this.integrityIndicators];
12532
+ }
12177
12533
  isRestartRequired() {
12178
12534
  return this.isRestartRequiredState;
12179
12535
  }
@@ -12267,7 +12623,7 @@ var init_OtaOrchestrator = __esm({
12267
12623
  error: "OTA native module disabled or missing"
12268
12624
  };
12269
12625
  }
12270
- __DEV__ && logger.debug("[OTA] Sync started\u2026");
12626
+ logger.debug("[OTA] Sync started\u2026");
12271
12627
  if (this.currentBundle?.id) {
12272
12628
  otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
12273
12629
  releaseId: this.currentBundle.releaseId,
@@ -12276,7 +12632,7 @@ var init_OtaOrchestrator = __esm({
12276
12632
  }
12277
12633
  const checkRes = await this.checkForUpdate(params);
12278
12634
  if (checkRes.action === "NONE") {
12279
- __DEV__ && logger.debug("[OTA] App is up to date");
12635
+ logger.debug("[OTA] App is up to date");
12280
12636
  this.startBootGuardHeartbeat();
12281
12637
  return {
12282
12638
  status: "UP_TO_DATE"
@@ -12319,7 +12675,7 @@ var init_OtaOrchestrator = __esm({
12319
12675
  version: bundle.version
12320
12676
  });
12321
12677
  const downloadStart = Date.now();
12322
- __DEV__ && logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
12678
+ logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
12323
12679
  let lastPostAt = 0;
12324
12680
  const postProgress = (bytesRead, status) => {
12325
12681
  const total = bundle.size > 0 ? bundle.size : Math.max(bytesRead, 1);
@@ -12397,7 +12753,7 @@ var init_OtaOrchestrator = __esm({
12397
12753
  staged = true;
12398
12754
  } else {
12399
12755
  if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
12400
- __DEV__ && logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
12756
+ logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
12401
12757
  try {
12402
12758
  staged = await NativeScaleBunOta_default.stagePatch(
12403
12759
  bundle.patch.url,
@@ -12407,7 +12763,7 @@ var init_OtaOrchestrator = __esm({
12407
12763
  );
12408
12764
  if (staged) {
12409
12765
  patchUsed = true;
12410
- __DEV__ && logger.debug("[OTA] Patch applied successfully");
12766
+ logger.debug("[OTA] Patch applied successfully");
12411
12767
  } else {
12412
12768
  logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
12413
12769
  }
@@ -12476,7 +12832,7 @@ var init_OtaOrchestrator = __esm({
12476
12832
  releaseId: bundle.releaseId,
12477
12833
  version: bundle.version
12478
12834
  });
12479
- __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
12835
+ logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
12480
12836
  await deliverOtaEvents(params);
12481
12837
  const installMode = bundle.installMode ?? "ON_NEXT_RESTART";
12482
12838
  if (params.autoRestart || installMode === "IMMEDIATE") {
@@ -12507,24 +12863,23 @@ var init_OtaOrchestrator = __esm({
12507
12863
  * native boot-guard marker. If JS crashes before the timer fires, the
12508
12864
  * marker stays set and native code reverts on next cold start.
12509
12865
  */
12510
- startBootGuardHeartbeat() {
12866
+ startBootGuardHeartbeat(preRead) {
12511
12867
  if (!NativeScaleBunOta_default) return;
12512
12868
  if (this.healthyTimer) {
12513
12869
  clearTimeout(this.healthyTimer);
12514
12870
  this.healthyTimer = null;
12515
12871
  }
12516
12872
  try {
12517
- const stateJson = NativeScaleBunOta_default.getSlotState();
12518
- const state = JSON.parse(stateJson);
12519
- if (!state.bootMarkerPresent) {
12873
+ const state = preRead !== void 0 ? preRead : JSON.parse(NativeScaleBunOta_default.getSlotState());
12874
+ if (!state?.bootMarkerPresent) {
12520
12875
  return;
12521
12876
  }
12522
12877
  const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
12523
- __DEV__ && logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
12878
+ logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
12524
12879
  this.healthyTimer = setTimeout(() => {
12525
12880
  noThrow(() => {
12526
12881
  NativeScaleBunOta_default?.markHealthy();
12527
- __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12882
+ logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
12528
12883
  this.healthyTimer = null;
12529
12884
  const running = this.currentBundle;
12530
12885
  if (running?.id) {
@@ -12570,11 +12925,11 @@ var init_OtaOrchestrator = __esm({
12570
12925
  } catch {
12571
12926
  }
12572
12927
  this.resumeRestartArmed = false;
12573
- __DEV__ && logger.info("[OTA] App resumed \u2014 activating the staged update now");
12928
+ logger.info("[OTA] App resumed \u2014 activating the staged update now");
12574
12929
  this.restart();
12575
12930
  }
12576
12931
  });
12577
- __DEV__ && logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
12932
+ logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
12578
12933
  } catch {
12579
12934
  this.resumeRestartArmed = false;
12580
12935
  }
@@ -12591,7 +12946,7 @@ var init_OtaOrchestrator = __esm({
12591
12946
  */
12592
12947
  restart() {
12593
12948
  noThrow(() => {
12594
- __DEV__ && logger.info("[OTA] Triggering app restart\u2026");
12949
+ logger.info("[OTA] Triggering app restart\u2026");
12595
12950
  if (NativeScaleBunOta_default && typeof NativeScaleBunOta_default.restartApp === "function") {
12596
12951
  NativeScaleBunOta_default.restartApp();
12597
12952
  } else if (import_react_native34.DevSettings && typeof import_react_native34.DevSettings.reload === "function") {
@@ -12610,17 +12965,25 @@ var init_OtaOrchestrator = __esm({
12610
12965
  var uiState_exports = {};
12611
12966
  __export(uiState_exports, {
12612
12967
  clearUiState: () => clearUiState,
12968
+ onUiStateChange: () => onUiStateChange,
12969
+ resetUiStateListeners: () => resetUiStateListeners,
12613
12970
  setUiState: () => setUiState,
12971
+ uiStateListenerCount: () => uiStateListenerCount,
12614
12972
  uiStateSignature: () => uiStateSignature
12615
12973
  });
12616
12974
  function setUiState(name, value) {
12617
12975
  const n = clean(name);
12618
12976
  const v = clean(value);
12619
- if (n && v) declared.set(n, v);
12977
+ if (!n || !v) return;
12978
+ if (declared.get(n) === v) return;
12979
+ declared.set(n, v);
12980
+ notifyChanged();
12620
12981
  }
12621
12982
  function clearUiState(name) {
12983
+ const before = declared.size;
12622
12984
  if (name === void 0) declared.clear();
12623
12985
  else declared.delete(clean(name));
12986
+ if (declared.size !== before) notifyChanged();
12624
12987
  }
12625
12988
  function uiStateSignature() {
12626
12989
  if (!declared.size) return void 0;
@@ -12633,12 +12996,42 @@ function uiStateSignature() {
12633
12996
  }
12634
12997
  return signature || void 0;
12635
12998
  }
12636
- var declared, clean;
12999
+ function onUiStateChange(listener) {
13000
+ listeners2.add(listener);
13001
+ return () => {
13002
+ listeners2.delete(listener);
13003
+ };
13004
+ }
13005
+ function resetUiStateListeners() {
13006
+ listeners2.clear();
13007
+ if (pending4 !== null) {
13008
+ clearTimeout(pending4);
13009
+ pending4 = null;
13010
+ }
13011
+ }
13012
+ function uiStateListenerCount() {
13013
+ return listeners2.size;
13014
+ }
13015
+ function notifyChanged() {
13016
+ if (pending4 !== null) return;
13017
+ pending4 = setTimeout(() => {
13018
+ pending4 = null;
13019
+ for (const listener of listeners2) {
13020
+ try {
13021
+ listener();
13022
+ } catch {
13023
+ }
13024
+ }
13025
+ }, 0);
13026
+ }
13027
+ var declared, clean, listeners2, pending4;
12637
13028
  var init_uiState = __esm({
12638
13029
  "lib/module/features/journey/uiState.js"() {
12639
13030
  "use strict";
12640
13031
  declared = /* @__PURE__ */ new Map();
12641
13032
  clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
13033
+ listeners2 = /* @__PURE__ */ new Set();
13034
+ pending4 = null;
12642
13035
  }
12643
13036
  });
12644
13037
 
@@ -14539,6 +14932,22 @@ var SHAPE = {
14539
14932
  }
14540
14933
  },
14541
14934
  captureInteractionHeatmap: bool(true),
14935
+ /**
14936
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
14937
+ * offset, not just ones wrapped in ScaleBunScrollView.
14938
+ *
14939
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
14940
+ * That flag changes what the SDK records about the host; this one changes what the HOST
14941
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
14942
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
14943
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
14944
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
14945
+ * discover afterwards.
14946
+ *
14947
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
14948
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
14949
+ */
14950
+ autoInstrumentScrollViews: bool(false),
14542
14951
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
14543
14952
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
14544
14953
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -16753,22 +17162,22 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
16753
17162
  * lose the snapshot entirely. `imageData.length` is the base64 length, which is within ~1% of the
16754
17163
  * serialized cost and needs no extra pass.
16755
17164
  */
16756
- static _selectFramesToPersist(pending4) {
17165
+ static _selectFramesToPersist(pending5) {
16757
17166
  const isKeyframe = (b) => b.frame.frameType !== "delta";
16758
17167
  const size = (b) => b.imageData?.length ?? 0;
16759
17168
  const keep = [];
16760
17169
  let budget = _BackendSessionAdapter.MAX_PERSIST_BYTES;
16761
17170
  const maxCount = _BackendSessionAdapter.MAX_FRAMES_PER_BATCH;
16762
- for (let i = pending4.length - 1; i >= 0; i--) {
16763
- const b = pending4[i];
17171
+ for (let i = pending5.length - 1; i >= 0; i--) {
17172
+ const b = pending5[i];
16764
17173
  if (!isKeyframe(b)) continue;
16765
17174
  const cost = size(b);
16766
17175
  if (cost > budget || keep.length >= maxCount) continue;
16767
17176
  budget -= cost;
16768
17177
  keep.push(b);
16769
17178
  }
16770
- for (let i = pending4.length - 1; i >= 0; i--) {
16771
- const b = pending4[i];
17179
+ for (let i = pending5.length - 1; i >= 0; i--) {
17180
+ const b = pending5[i];
16772
17181
  if (isKeyframe(b)) continue;
16773
17182
  const cost = size(b);
16774
17183
  if (cost > budget || keep.length >= maxCount) continue;
@@ -17201,6 +17610,10 @@ var SDKBootstrapper = class {
17201
17610
  return null;
17202
17611
  }
17203
17612
  this.config = result.data;
17613
+ const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
17614
+ if (typeof requestedApiBaseUrl === "string" && requestedApiBaseUrl && requestedApiBaseUrl !== DEFAULT_API_BASE_URL) {
17615
+ logger.warn(`[ScaleBun] Ignoring apiBaseUrl '${requestedApiBaseUrl}' \u2014 the endpoint is locked at build time and this build targets ${DEFAULT_API_BASE_URL}. To target another backend, change DEFAULT_API_BASE_URL in the SDK source and rebuild.`);
17616
+ }
17204
17617
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
17205
17618
  if (this.config.logLevel) {
17206
17619
  logger.setLevelByName(this.config.logLevel);
@@ -17365,6 +17778,20 @@ var SDKBootstrapper = class {
17365
17778
  }));
17366
17779
  }
17367
17780
  await this.featureRegistry.initializeAll(featureContext);
17781
+ if (this.config.autoInstrumentScrollViews === true) {
17782
+ try {
17783
+ const {
17784
+ autoInstrumentScrollViews: autoInstrumentScrollViews2
17785
+ } = (init_autoInstrumentScroll(), __toCommonJS(autoInstrumentScroll_exports));
17786
+ const outcome = autoInstrumentScrollViews2();
17787
+ if (!outcome.installed) {
17788
+ logger.warn("[Bootstrap] autoInstrumentScrollViews was requested but could not be installed: " + outcome.reason + " \u2014 taps will carry no content depth.");
17789
+ } else {
17790
+ __DEV__ && logger.debug("[Bootstrap] ScrollView auto-instrumentation installed");
17791
+ }
17792
+ } catch {
17793
+ }
17794
+ }
17368
17795
  if (features.replay !== false) {
17369
17796
  try {
17370
17797
  __DEV__ && logger.debug("[Bootstrap] Initializing unified SessionManager");
@@ -20008,14 +20435,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
20008
20435
  this._apiBaseUrl = result.config.apiBaseUrl;
20009
20436
  this._maybeStartEventTracker(rawConfig);
20010
20437
  if (this._pendingIdentity) {
20011
- const pending4 = this._pendingIdentity;
20438
+ const pending5 = this._pendingIdentity;
20012
20439
  this._pendingIdentity = null;
20013
- this.identify(pending4.userId, pending4.traits);
20440
+ this.identify(pending5.userId, pending5.traits);
20014
20441
  }
20015
20442
  const realQueue = coreContainer.get("PersistentQueue");
20016
20443
  const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
20017
20444
  if (buffered.length > 0) {
20018
- __DEV__ && logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
20445
+ logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
20019
20446
  for (const envelope of buffered) {
20020
20447
  realQueue.add(envelope);
20021
20448
  }
@@ -20106,7 +20533,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
20106
20533
  }
20107
20534
  const clientKey = this._clientKey;
20108
20535
  const apiUrl = this._apiBaseUrl;
20109
- if (!clientKey || !apiUrl) return;
20536
+ if (!clientKey || !apiUrl) {
20537
+ return;
20538
+ }
20110
20539
  this._otaCheckInFlight = true;
20111
20540
  try {
20112
20541
  const info = await bridgeAdapter.getDeviceInfo();
@@ -20157,7 +20586,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20157
20586
  if (!dbgConfig) return;
20158
20587
  dbgConfig.__skipDeprecationWarning = true;
20159
20588
  this.enableDebug(dbgConfig);
20160
- __DEV__ && logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
20589
+ logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
20161
20590
  } catch (err) {
20162
20591
  logger.warn("[ScaleBun] Auto debug connection failed:", err?.message);
20163
20592
  }
@@ -20171,7 +20600,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20171
20600
  const apiBaseUrl = this._apiBaseUrl;
20172
20601
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
20173
20602
  if (enabled && !appId) {
20174
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
20603
+ logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
20175
20604
  }
20176
20605
  return;
20177
20606
  }
@@ -20221,7 +20650,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20221
20650
  onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue)
20222
20651
  });
20223
20652
  this._eventTracker.start();
20224
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
20653
+ logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
20225
20654
  this._applyNativeDeviceContext(this._eventTracker);
20226
20655
  this._captureInstallReferrer(this._eventTracker);
20227
20656
  const tracker = this._eventTracker;
@@ -20855,7 +21284,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
20855
21284
  const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
20856
21285
  const messages = cfg?.inAppMessages ?? [];
20857
21286
  const previews = messages.filter((m) => m.preview === true || m.isTest === true);
20858
- __DEV__ && logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
21287
+ logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
20859
21288
  return messages;
20860
21289
  },
20861
21290
  /** Upload a survey/NPS/feedback response (idempotent, rides the outbox). */
@@ -21131,7 +21560,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21131
21560
  logger.warn("SDK not initialized. Event dropped:", name);
21132
21561
  return;
21133
21562
  }
21134
- __DEV__ && logger.debug("Tracking event:", name);
21563
+ logger.debug("Tracking event:", name);
21135
21564
  engageSignals.emit(name, payload);
21136
21565
  const adapter = SessionManager.getExistingInstance()?.getBackendTransport();
21137
21566
  if (!isUploadedSessionEvent(name)) {
@@ -21260,7 +21689,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21260
21689
  await Promise.all([this._eventTracker?.flush() ?? Promise.resolve(), adapter?.flushAnalytics() ?? Promise.resolve()]);
21261
21690
  return;
21262
21691
  }
21263
- __DEV__ && logger.debug("Flushing events from persistent queue...");
21692
+ logger.debug("Flushing events from persistent queue...");
21264
21693
  const httpClient = coreContainer.get("HttpClient");
21265
21694
  await flushQueue(this._getQueue(), httpClient, getDeviceId());
21266
21695
  });
@@ -21282,9 +21711,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
21282
21711
  const bootstrapPerfFeature = getPerformanceFeature();
21283
21712
  if (bootstrapPerfFeature) {
21284
21713
  this._performanceFeature = bootstrapPerfFeature;
21285
- __DEV__ && logger.debug("Performance feature linked from bootstrap");
21714
+ logger.debug("Performance feature linked from bootstrap");
21286
21715
  }
21287
- __DEV__ && logger.info("Debug enabled");
21716
+ logger.info("Debug enabled");
21288
21717
  });
21289
21718
  }
21290
21719
  /** Disable the debug subsystem and clean up */
@@ -21427,14 +21856,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
21427
21856
  return false;
21428
21857
  }
21429
21858
  sm.startSession(metadata);
21430
- __DEV__ && logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
21859
+ logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
21431
21860
  return true;
21432
21861
  },
21433
21862
  end: (reason) => {
21434
21863
  const sm = SessionManager.getExistingInstance();
21435
21864
  if (!sm || !sm.isActive) return false;
21436
21865
  sm.endSession(reason);
21437
- __DEV__ && logger.info("session.end() \u2705");
21866
+ logger.info("session.end() \u2705");
21438
21867
  return true;
21439
21868
  },
21440
21869
  emitEvent: (type, data) => {
@@ -21487,7 +21916,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21487
21916
  ...this._privacyConfig,
21488
21917
  ...config
21489
21918
  };
21490
- __DEV__ && logger.debug("[report] Privacy config updated");
21919
+ logger.debug("[report] Privacy config updated");
21491
21920
  },
21492
21921
  /** Set branding for exported reports. */
21493
21922
  setBranding: (config) => {
@@ -21495,7 +21924,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
21495
21924
  ...this._brandingConfig,
21496
21925
  ...config
21497
21926
  };
21498
- __DEV__ && logger.debug("[report] Branding config updated");
21927
+ logger.debug("[report] Branding config updated");
21499
21928
  },
21500
21929
  /** Get current privacy config. */
21501
21930
  getPrivacy: () => ({
@@ -21542,9 +21971,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
21542
21971
  AutoScreenDetector: AutoScreenDetector2
21543
21972
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
21544
21973
  AutoScreenDetector2.getInstance().setNavigationRef(ref);
21545
- __DEV__ && logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
21974
+ logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
21546
21975
  } catch (err) {
21547
- __DEV__ && logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
21976
+ logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
21548
21977
  }
21549
21978
  });
21550
21979
  }
@@ -21851,8 +22280,8 @@ function installEngageTriggerEngine(deps) {
21851
22280
  if (!cachedConfig) return;
21852
22281
  configReady = true;
21853
22282
  const now2 = Date.now();
21854
- const pending4 = recent.splice(0);
21855
- for (const e of pending4) {
22283
+ const pending5 = recent.splice(0);
22284
+ for (const e of pending5) {
21856
22285
  const age = now2 - e.at;
21857
22286
  if (age > RECENT_TTL_MS) continue;
21858
22287
  const remaining = delayMsForTrigger(cachedConfig, e.options.trigger) - age;
@@ -23117,11 +23546,11 @@ var keyOf = (value) => value.trim();
23117
23546
  var validRect = (rect) => [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
23118
23547
  function createEngageAnchorRegistry() {
23119
23548
  const entries = /* @__PURE__ */ new Map();
23120
- const listeners2 = /* @__PURE__ */ new Map();
23549
+ const listeners3 = /* @__PURE__ */ new Map();
23121
23550
  const notify = (rawKey) => {
23122
23551
  const key = keyOf(rawKey);
23123
23552
  if (!key) return;
23124
- listeners2.get(key)?.forEach((listener) => listener());
23553
+ listeners3.get(key)?.forEach((listener) => listener());
23125
23554
  };
23126
23555
  return {
23127
23556
  register(rawKey, node) {
@@ -23163,12 +23592,12 @@ function createEngageAnchorRegistry() {
23163
23592
  const key = keyOf(rawKey);
23164
23593
  if (!key) return () => {
23165
23594
  };
23166
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
23595
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
23167
23596
  set.add(listener);
23168
- listeners2.set(key, set);
23597
+ listeners3.set(key, set);
23169
23598
  return () => {
23170
23599
  set.delete(listener);
23171
- if (set.size === 0) listeners2.delete(key);
23600
+ if (set.size === 0) listeners3.delete(key);
23172
23601
  };
23173
23602
  },
23174
23603
  notify
@@ -32277,10 +32706,10 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
32277
32706
  var keyOf2 = (value) => value.trim();
32278
32707
  function createEngageInlinePlacementRegistry() {
32279
32708
  const slots = /* @__PURE__ */ new Map();
32280
- const pending4 = /* @__PURE__ */ new Map();
32281
- const listeners2 = /* @__PURE__ */ new Map();
32709
+ const pending5 = /* @__PURE__ */ new Map();
32710
+ const listeners3 = /* @__PURE__ */ new Map();
32282
32711
  const notify = (key) => {
32283
- listeners2.get(key)?.forEach((listener) => listener());
32712
+ listeners3.get(key)?.forEach((listener) => listener());
32284
32713
  };
32285
32714
  return {
32286
32715
  register(rawKey, sink) {
@@ -32297,7 +32726,7 @@ function createEngageInlinePlacementRegistry() {
32297
32726
  sink,
32298
32727
  token
32299
32728
  });
32300
- sink(pending4.get(key) ?? null);
32729
+ sink(pending5.get(key) ?? null);
32301
32730
  if (!previous) notify(key);
32302
32731
  return () => {
32303
32732
  if (slots.get(key)?.token !== token) return;
@@ -32308,7 +32737,7 @@ function createEngageInlinePlacementRegistry() {
32308
32737
  deliver(rawKey, delivery) {
32309
32738
  const key = keyOf2(rawKey);
32310
32739
  if (!key) return false;
32311
- pending4.set(key, delivery);
32740
+ pending5.set(key, delivery);
32312
32741
  const slot = slots.get(key);
32313
32742
  slot?.sink(delivery);
32314
32743
  return slot !== void 0;
@@ -32316,9 +32745,9 @@ function createEngageInlinePlacementRegistry() {
32316
32745
  clear(rawKey, messageId) {
32317
32746
  const key = keyOf2(rawKey);
32318
32747
  if (!key) return;
32319
- const current = pending4.get(key);
32748
+ const current = pending5.get(key);
32320
32749
  if (!current || messageId !== void 0 && current.message?.id !== messageId) return;
32321
- pending4.delete(key);
32750
+ pending5.delete(key);
32322
32751
  slots.get(key)?.sink(null);
32323
32752
  },
32324
32753
  has(rawKey) {
@@ -32329,12 +32758,12 @@ function createEngageInlinePlacementRegistry() {
32329
32758
  const key = keyOf2(rawKey);
32330
32759
  if (!key) return () => {
32331
32760
  };
32332
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
32761
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
32333
32762
  set.add(listener);
32334
- listeners2.set(key, set);
32763
+ listeners3.set(key, set);
32335
32764
  return () => {
32336
32765
  set.delete(listener);
32337
- if (set.size === 0) listeners2.delete(key);
32766
+ if (set.size === 0) listeners3.delete(key);
32338
32767
  };
32339
32768
  }
32340
32769
  };
@@ -33123,6 +33552,7 @@ var import_react_native43 = require("react-native");
33123
33552
  // lib/module/features/journey/touchTarget.js
33124
33553
  var MAX_DEPTH = 12;
33125
33554
  var MAX_LEN = 60;
33555
+ var MAX_PATH_SEGMENTS = 4;
33126
33556
  function str2(v) {
33127
33557
  if (typeof v !== "string") return void 0;
33128
33558
  const s = v.trim();
@@ -33154,41 +33584,136 @@ function fiberOf(event) {
33154
33584
  const e = event;
33155
33585
  const direct = e._targetInst;
33156
33586
  if (direct && typeof direct === "object") return direct;
33157
- const target = e.target;
33158
- if (target && typeof target === "object") {
33159
- const handle = target._internalFiberInstanceHandleDEV ?? target._internalInstanceHandle;
33587
+ const target = e.target ?? e.nativeEvent?.target;
33588
+ if (target) {
33589
+ const handle = target._internalInstanceHandle ?? target._targetInst;
33160
33590
  if (handle && typeof handle === "object") return handle;
33161
33591
  }
33162
33592
  return null;
33163
33593
  }
33164
- function resolveTouchTarget(event, maxDepth = MAX_DEPTH) {
33594
+ function resolveTouchTarget(event, optsOrMaxDepth = {}) {
33595
+ const opts = typeof optsOrMaxDepth === "number" ? {
33596
+ maxDepth: optsOrMaxDepth
33597
+ } : optsOrMaxDepth;
33598
+ const maxDepth = opts.maxDepth ?? MAX_DEPTH;
33165
33599
  let f = fiberOf(event);
33166
33600
  if (!f) return null;
33167
33601
  const out = {};
33602
+ const segments = [];
33603
+ const ancestors = [];
33168
33604
  let depth = 0;
33169
33605
  try {
33170
33606
  while (f && depth < maxDepth) {
33171
33607
  const props = f.memoizedProps;
33172
33608
  if (props) {
33173
- out.accessibilityLabel ??= str2(props.accessibilityLabel);
33609
+ const beforeTestId = out.testID;
33174
33610
  out.testID ??= str2(props.testID);
33175
- out.text ??= textChild(props);
33611
+ if (!beforeTestId && out.testID) out.node = f.stateNode;
33612
+ if (typeof props.cellKey === "string" && props.cellKey) {
33613
+ out.itemKey ??= str2(props.cellKey);
33614
+ if (typeof props.index === "number" && Number.isFinite(props.index)) {
33615
+ out.itemIndex ??= props.index;
33616
+ }
33617
+ }
33618
+ if (opts.includeText) {
33619
+ out.accessibilityLabel ??= str2(props.accessibilityLabel);
33620
+ out.text ??= textChild(props);
33621
+ }
33622
+ }
33623
+ if (f.stateNode) ancestors.push(f.stateNode);
33624
+ const name = componentName(f);
33625
+ if (name) {
33626
+ out.component ??= name;
33627
+ if (segments[segments.length - 1] !== name && segments.length < MAX_PATH_SEGMENTS) {
33628
+ segments.push(name);
33629
+ }
33176
33630
  }
33177
- out.component ??= componentName(f);
33178
- if (out.accessibilityLabel && out.testID) break;
33631
+ if (out.testID && segments.length >= MAX_PATH_SEGMENTS) break;
33179
33632
  f = f.return ?? null;
33180
33633
  depth++;
33181
33634
  }
33182
33635
  } catch {
33183
33636
  }
33184
- return out.accessibilityLabel || out.testID || out.text || out.component ? out : null;
33637
+ if (segments.length) out.path = segments.join("<");
33638
+ if (!out.node) out.node = fiberOf(event)?.stateNode;
33639
+ if (ancestors.length) out.ancestors = ancestors;
33640
+ return out.testID || out.path || out.component || out.accessibilityLabel || out.text || out.itemKey ? out : null;
33185
33641
  }
33186
33642
  function describeTouchTarget(t) {
33187
33643
  if (!t) return void 0;
33188
- return t.accessibilityLabel || t.testID || t.text || t.component;
33644
+ return t.testID || t.path || t.component;
33645
+ }
33646
+ function targetIdentity(t) {
33647
+ if (!t) return {
33648
+ source: "none"
33649
+ };
33650
+ if (t.testID) return {
33651
+ id: t.testID,
33652
+ source: "testID"
33653
+ };
33654
+ if (t.path) return {
33655
+ id: t.path,
33656
+ source: "path"
33657
+ };
33658
+ if (t.component) return {
33659
+ id: t.component,
33660
+ source: "component"
33661
+ };
33662
+ return {
33663
+ source: "none"
33664
+ };
33665
+ }
33666
+
33667
+ // lib/module/features/journey/targetGeometry.js
33668
+ function readRectSync(node) {
33669
+ if (!node || typeof node !== "object") return null;
33670
+ const n = node;
33671
+ const fn = typeof n.unstable_getBoundingClientRect === "function" ? n.unstable_getBoundingClientRect : typeof n.getBoundingClientRect === "function" ? n.getBoundingClientRect : null;
33672
+ if (!fn) return null;
33673
+ try {
33674
+ const r = fn.call(n);
33675
+ if (!r) return null;
33676
+ const {
33677
+ x,
33678
+ y,
33679
+ width,
33680
+ height
33681
+ } = r;
33682
+ if (![x, y, width, height].every((v) => typeof v === "number" && Number.isFinite(v))) return null;
33683
+ if (width <= 0 || height <= 0) return null;
33684
+ return {
33685
+ x,
33686
+ y,
33687
+ width,
33688
+ height
33689
+ };
33690
+ } catch {
33691
+ return null;
33692
+ }
33693
+ }
33694
+ function normalizeRect(rect, base) {
33695
+ if (!rect || !base || base.w <= 0 || base.h <= 0) return null;
33696
+ const out = {
33697
+ x: rect.x / base.w,
33698
+ y: rect.y / base.h,
33699
+ width: rect.width / base.w,
33700
+ height: rect.height / base.h
33701
+ };
33702
+ if (![out.x, out.y, out.width, out.height].every((v) => Number.isFinite(v))) return null;
33703
+ return out;
33704
+ }
33705
+ function localPoint(rect, nx, ny) {
33706
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
33707
+ if (typeof nx !== "number" || typeof ny !== "number") return null;
33708
+ if (!Number.isFinite(nx) || !Number.isFinite(ny)) return null;
33709
+ return {
33710
+ u: (nx - rect.x) / rect.width,
33711
+ v: (ny - rect.y) / rect.height
33712
+ };
33189
33713
  }
33190
33714
 
33191
33715
  // lib/module/features/journey/ScaleBunDebugRoot.js
33716
+ init_scrollContext();
33192
33717
  init_device();
33193
33718
  init_interactionProtocol();
33194
33719
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -33200,6 +33725,7 @@ function getBootstrap() {
33200
33725
  return _bootstrap;
33201
33726
  }
33202
33727
  var JourneyContext = /* @__PURE__ */ (0, import_react9.createContext)(null);
33728
+ var UI_STATE_CAPTURE_MIN_INTERVAL_MS = 1e3;
33203
33729
  function ScaleBunDebugRoot({
33204
33730
  children,
33205
33731
  navigationRef,
@@ -33378,6 +33904,28 @@ function ScaleBunDebugRoot({
33378
33904
  clearTimeout(timer);
33379
33905
  };
33380
33906
  }, [navigationRef]);
33907
+ (0, import_react9.useEffect)(() => {
33908
+ let lastCaptureTs = 0;
33909
+ const {
33910
+ onUiStateChange: onUiStateChange2
33911
+ } = (init_uiState(), __toCommonJS(uiState_exports));
33912
+ const unsubscribe = onUiStateChange2(() => {
33913
+ const now2 = Date.now();
33914
+ if (now2 - lastCaptureTs < UI_STATE_CAPTURE_MIN_INTERVAL_MS) return;
33915
+ lastCaptureTs = now2;
33916
+ try {
33917
+ const {
33918
+ SessionManager: SessionManager2
33919
+ } = (init_SessionManager(), __toCommonJS(SessionManager_exports));
33920
+ const sm = SessionManager2.getExistingInstance();
33921
+ if (sm && typeof sm.captureManualFrame === "function") {
33922
+ void sm.captureManualFrame();
33923
+ }
33924
+ } catch {
33925
+ }
33926
+ });
33927
+ return unsubscribe;
33928
+ }, []);
33381
33929
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
33382
33930
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
33383
33931
  const touchStartRef = (0, import_react9.useRef)(null);
@@ -33394,10 +33942,15 @@ function ScaleBunDebugRoot({
33394
33942
  lastNativeTouchTsRef.current = Date.now();
33395
33943
  nativeTrackingConfirmedRef.current = true;
33396
33944
  const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
33397
- const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
33945
+ pruneInteractionStarts(interactionStartsRef.current, nativeOccurredAt);
33946
+ const correlation = correlateInteraction(interactionStartsRef.current, nativeOccurredAt);
33947
+ const start = correlation.start;
33948
+ if (correlation.index >= 0) {
33949
+ interactionStartsRef.current.splice(correlation.index, 1);
33950
+ }
33398
33951
  if (start) {
33399
- const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
33400
- if (pending4 !== void 0) clearTimeout(pending4);
33952
+ const pending5 = pendingJsEmitsRef.current.get(start.interactionId);
33953
+ if (pending5 !== void 0) clearTimeout(pending5);
33401
33954
  pendingJsEmitsRef.current.delete(start.interactionId);
33402
33955
  }
33403
33956
  const {
@@ -33420,11 +33973,62 @@ function ScaleBunDebugRoot({
33420
33973
  interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
33421
33974
  interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
33422
33975
  occurredAt: nativeOccurredAt,
33976
+ /**
33977
+ * RN-6 — THESE FIVE FIELDS ARE THE ONLY ONES THE JOIN CARRIES, AND
33978
+ * THEREFORE THE ONLY ONES A BAD JOIN CAN FALSIFY.
33979
+ *
33980
+ * Coordinates below come from native and are right regardless. So a
33981
+ * mispairing does not produce visibly broken data: it produces heat
33982
+ * in the correct position attributed to the wrong control, in the
33983
+ * wrong UI state, on the wrong screen. Every field is individually
33984
+ * well-formed and nothing downstream can tell.
33985
+ *
33986
+ * When the correlation is not 'exact' they are left UNDEFINED rather
33987
+ * than filled from a best guess. An interaction with no target is
33988
+ * honest and visibly incomplete — the accounting lane can count it as
33989
+ * unplaceable and say why. An interaction with the WRONG target is
33990
+ * indistinguishable from a correct one and silently corrupts every
33991
+ * aggregate it enters. `null` is not `0`, and a guess is not a
33992
+ * measurement.
33993
+ */
33423
33994
  ui: start?.ui,
33424
33995
  stateStatus: start?.stateStatus ?? "not_captured",
33425
33996
  target: start?.target,
33426
33997
  targetId: start?.targetId,
33998
+ targetSource: start?.targetSource,
33427
33999
  screenName: start?.screenName,
34000
+ screenSource: start?.screenSource ?? "none",
34001
+ targetRect: start?.targetRect,
34002
+ /**
34003
+ * RN-15 — WHERE IN THE CONTROL, computed where both halves exist: the
34004
+ * point comes from the native event and the rect was measured at
34005
+ * finger-down, in the same capture base. Pairing them here rather than
34006
+ * downstream stops anything later reaching for a different rect.
34007
+ */
34008
+ ...(() => {
34009
+ const lp = localPoint(start?.targetRect ?? null, event.normalizedX, event.normalizedY);
34010
+ return lp ? {
34011
+ localU: lp.u,
34012
+ localV: lp.v
34013
+ } : {};
34014
+ })(),
34015
+ scrollX: start?.scrollX,
34016
+ scrollY: start?.scrollY,
34017
+ scrollSource: start?.scrollSource ?? "none",
34018
+ itemKey: start?.itemKey,
34019
+ itemIndex: start?.itemIndex,
34020
+ /**
34021
+ * The join's own verdict, shipped with the row so it is auditable
34022
+ * rather than a claim made in a comment. 'ambiguous' and 'none' are
34023
+ * deliberately distinguished: 'none' means the JS handler never fired
34024
+ * (a real capture gap on this platform), 'ambiguous' means it fired
34025
+ * more than once too close together to attribute. Those have
34026
+ * different fixes, and collapsing them would hide which one is
34027
+ * actually happening in production.
34028
+ */
34029
+ correlation: correlation.outcome,
34030
+ correlationDeltaMs: correlation.deltaMs,
34031
+ correlationRunnerUpDeltaMs: correlation.runnerUpDeltaMs,
33428
34032
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33429
34033
  x: event.rawX,
33430
34034
  y: event.rawY,
@@ -33496,6 +34100,7 @@ function ScaleBunDebugRoot({
33496
34100
  const target = resolveTouchTarget(e);
33497
34101
  let ui;
33498
34102
  let screenName;
34103
+ let screenSource = "none";
33499
34104
  try {
33500
34105
  const {
33501
34106
  uiStateSignature: uiStateSignature2
@@ -33507,7 +34112,9 @@ function ScaleBunDebugRoot({
33507
34112
  const {
33508
34113
  AutoScreenDetector: AutoScreenDetector2
33509
34114
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
33510
- screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
34115
+ const detector = AutoScreenDetector2.getInstance();
34116
+ screenName = detector.getCurrentScreen() || void 0;
34117
+ screenSource = screenName ? detector.getCurrentScreenSource() : "none";
33511
34118
  } catch {
33512
34119
  }
33513
34120
  const start = {
@@ -33517,9 +34124,91 @@ function ScaleBunDebugRoot({
33517
34124
  y: touch.pageY,
33518
34125
  locationX: touch.locationX,
33519
34126
  locationY: touch.locationY,
34127
+ /**
34128
+ * RN-1 + RN-2 — the identity that is persisted, and it is now code-derived.
34129
+ *
34130
+ * `describeTouchTarget` returns `testID` -> structural path -> component name,
34131
+ * and never a caption or an accessibilityLabel. Before RN-2 it preferred free
34132
+ * text, so this line was writing user data ("Pay EGP 1,442", an order row
34133
+ * labelled with an email address, a contact's name) into the analytics store —
34134
+ * and, because `target` is an aggregation key, splitting one button into one
34135
+ * bucket per person at the same time.
34136
+ *
34137
+ * `targetId` used to be `target?.testID` alone, which is `undefined` in the
34138
+ * overwhelming majority of apps — no testIDs authored — so the field the
34139
+ * backend keys on was empty almost always. It now falls back to the structural
34140
+ * path, with `targetSource` recording WHICH it got.
34141
+ */
33520
34142
  target: describeTouchTarget(target),
33521
- targetId: target?.testID,
34143
+ targetId: targetIdentity(target).id,
34144
+ /**
34145
+ * WHICH KIND of identity this is, because they are not equally trustworthy: a
34146
+ * `testID` is a promise from the app's authors that this control is this
34147
+ * control; a `path` is an inference that breaks when the tree is refactored.
34148
+ * Without this the accounting lane cannot distinguish "the control had no
34149
+ * identity" from "the identity is a structural guess", and both would render
34150
+ * as an equally confident target.
34151
+ */
34152
+ targetSource: targetIdentity(target).source,
34153
+ /* RN-4 — row identity from the app's own keyExtractor; see touchTarget.ts. */
34154
+ itemKey: target?.itemKey,
34155
+ itemIndex: target?.itemIndex,
34156
+ /**
34157
+ * RN-13 — the control's rectangle, so `heat ∈ target` is decidable.
34158
+ *
34159
+ * Read SYNCHRONOUSLY here, in the same tick as the touch, and normalized against
34160
+ * the SAME measured root the tap's own coordinates use. Both halves matter:
34161
+ *
34162
+ * - A callback measurement (`measureInWindow`) would resolve after this handler
34163
+ * returns, so the rect attached to a tap could be the previous tap's, with
34164
+ * nothing afterwards able to tell. `targetGeometry` refuses to guess: it reads
34165
+ * now or reports null.
34166
+ * - Normalizing against a window size instead of the root view is off by the
34167
+ * status bar on most devices, which would read as "the heat is just outside
34168
+ * the button" everywhere rather than as a measurement bug.
34169
+ *
34170
+ * `measureRoot()` is called first because the cached root rect is populated
34171
+ * asynchronously at mount; without it the first taps of a session normalize
34172
+ * against nothing and get a null rect for a reason that has nothing to do with
34173
+ * the control.
34174
+ */
34175
+ /**
34176
+ * RN-3 — WHERE IN THE CONTENT, not just where on the glass.
34177
+ *
34178
+ * Two taps at the same screen coordinate one viewport apart are two different
34179
+ * controls. Without an offset they merge into one hotspot, and the heatmap shows
34180
+ * heavy engagement at a position where, in content terms, nothing in particular
34181
+ * is. The offset is also a CACHE dimension — two scroll states of one route must
34182
+ * not share a backdrop — so its absence corrupts the picture as well as the count.
34183
+ *
34184
+ * `ancestors` is innermost-first, so this takes the NEAREST scroller that has a
34185
+ * recorded position: a tap inside a horizontal carousel belongs to the carousel's
34186
+ * content, not to how far the page behind it had scrolled.
34187
+ *
34188
+ * Null when no ancestor has one — never zero. Zero is a real scroll position (the
34189
+ * top of a list), and reporting it for "unknown" would place every unmeasured tap
34190
+ * at the top of its content, indistinguishable from a genuine tap there.
34191
+ */
34192
+ ...(() => {
34193
+ const hit = nearestRecordedOffset((target?.ancestors ?? []).map((n) => nativeTagOf(n)));
34194
+ return hit ? {
34195
+ scrollX: hit.x,
34196
+ scrollY: hit.y,
34197
+ scrollSource: "wrapper"
34198
+ } : {
34199
+ scrollSource: "none"
34200
+ };
34201
+ })(),
34202
+ targetRect: (() => {
34203
+ measureRoot();
34204
+ const root = rootRectRef.current;
34205
+ return normalizeRect(readRectSync(target?.node), root ? {
34206
+ w: root.w,
34207
+ h: root.h
34208
+ } : null) ?? void 0;
34209
+ })(),
33522
34210
  screenName,
34211
+ screenSource,
33523
34212
  ui,
33524
34213
  stateStatus: ui ? "captured_nonempty" : "not_instrumented",
33525
34214
  emitAutomaticAnalytics: captureAutomaticInteractions
@@ -33556,7 +34245,12 @@ function ScaleBunDebugRoot({
33556
34245
  }
33557
34246
  measureRoot();
33558
34247
  const rootRect = rootRectRef.current;
33559
- const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
34248
+ const freshTarget = start ? null : resolveTouchTarget(e);
34249
+ const tapped = start?.target ?? describeTouchTarget(freshTarget);
34250
+ const identity = start ? {
34251
+ id: start.targetId,
34252
+ source: start.targetSource
34253
+ } : targetIdentity(freshTarget);
33560
34254
  const gestureDetails = {
33561
34255
  target: tapped,
33562
34256
  interactionId: start?.interactionId ?? generateInteractionId(),
@@ -33564,8 +34258,16 @@ function ScaleBunDebugRoot({
33564
34258
  occurredAt: start?.occurredAt ?? Date.now(),
33565
34259
  ui: start?.ui,
33566
34260
  stateStatus: start?.stateStatus ?? "not_captured",
33567
- targetId: start?.targetId,
34261
+ targetId: identity.id,
34262
+ targetSource: identity.source,
33568
34263
  screenName: start?.screenName,
34264
+ screenSource: start?.screenSource ?? "none",
34265
+ targetRect: start?.targetRect,
34266
+ scrollX: start?.scrollX,
34267
+ scrollY: start?.scrollY,
34268
+ scrollSource: start?.scrollSource ?? "none",
34269
+ itemKey: start?.itemKey,
34270
+ itemIndex: start?.itemIndex,
33569
34271
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
33570
34272
  x: start?.x ?? touch.pageX,
33571
34273
  y: start?.y ?? touch.pageY,
@@ -33581,6 +34283,11 @@ function ScaleBunDebugRoot({
33581
34283
  const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
33582
34284
  gestureDetails.normalizedX = nx;
33583
34285
  gestureDetails.normalizedY = ny;
34286
+ const lp = localPoint(start?.targetRect ?? null, nx, ny);
34287
+ if (lp) {
34288
+ gestureDetails.localU = lp.u;
34289
+ gestureDetails.localV = lp.v;
34290
+ }
33584
34291
  gestureDetails.normalizedPrecomputed = true;
33585
34292
  gestureDetails.viewportWidth = rootRect.w;
33586
34293
  gestureDetails.viewportHeight = rootRect.h;
@@ -33709,6 +34416,13 @@ function useScaleBunScreen(name) {
33709
34416
  }
33710
34417
  } catch {
33711
34418
  }
34419
+ try {
34420
+ const {
34421
+ AutoScreenDetector: AutoScreenDetector2
34422
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
34423
+ AutoScreenDetector2.getInstance().setManualScreen(name);
34424
+ } catch {
34425
+ }
33712
34426
  try {
33713
34427
  const bs = getBootstrap();
33714
34428
  const perfFeature = bs?.getPerformanceFeature?.();
@@ -33859,6 +34573,18 @@ var ScaleBunScrollView = /* @__PURE__ */ import_react12.default.forwardRef(funct
33859
34573
  }
33860
34574
  } catch {
33861
34575
  }
34576
+ try {
34577
+ const {
34578
+ recordScrollOffset: recordScrollOffset2,
34579
+ nativeTagOf: nativeTagOf2
34580
+ } = (init_scrollContext(), __toCommonJS(scrollContext_exports));
34581
+ const {
34582
+ contentOffset,
34583
+ target
34584
+ } = event.nativeEvent;
34585
+ recordScrollOffset2(typeof target === "number" ? target : nativeTagOf2(target), contentOffset.x, contentOffset.y);
34586
+ } catch {
34587
+ }
33862
34588
  if (userOnScroll) {
33863
34589
  userOnScroll(event);
33864
34590
  }