@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
@@ -600,7 +600,7 @@ var SDK_VERSION;
600
600
  var init_version = __esm({
601
601
  "lib/module/core/constants/version.js"() {
602
602
  "use strict";
603
- SDK_VERSION = "2.0.0";
603
+ SDK_VERSION = "2.0.1";
604
604
  }
605
605
  });
606
606
 
@@ -1353,6 +1353,15 @@ var init_AutoScreenDetector = __esm({
1353
1353
  _manualScreen = null;
1354
1354
  _manualScreenTs = 0;
1355
1355
  _autoScreen = null;
1356
+ /**
1357
+ * When `_autoScreen` was last written.
1358
+ *
1359
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
1360
+ * Without a timestamp the only comparison available was "is the manual value still
1361
+ * fresh", which is a different question and gave the wrong answer — see the note on
1362
+ * `_currentScreen()`.
1363
+ */
1364
+ _autoScreenTs = 0;
1356
1365
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
1357
1366
  _autoVisitSig = null;
1358
1367
  _navigationRef = null;
@@ -1420,6 +1429,7 @@ var init_AutoScreenDetector = __esm({
1420
1429
  if (this.isManualScreenActive()) return;
1421
1430
  if (name !== this._autoScreen) {
1422
1431
  this._autoScreen = name;
1432
+ this._autoScreenTs = Date.now();
1423
1433
  this._autoVisitSig = name;
1424
1434
  this._emitScreenChange(name, "heuristic");
1425
1435
  }
@@ -1427,8 +1437,19 @@ var init_AutoScreenDetector = __esm({
1427
1437
  }
1428
1438
  }
1429
1439
  /**
1430
- * Check if a manual screen is currently active.
1431
- * Used by auto-detection to yield to manual overrides.
1440
+ * Should a WRITER defer to the manual screen right now?
1441
+ *
1442
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
1443
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
1444
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
1445
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
1446
+ *
1447
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
1448
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
1449
+ * `setManualScreen` in production, so the effect was latent — but the moment the
1450
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
1451
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
1452
+ * fix that introduced that would have been worse than the gap it closed.
1432
1453
  */
1433
1454
  isManualScreenActive() {
1434
1455
  if (!this._manualScreen) return false;
@@ -1474,6 +1495,7 @@ var init_AutoScreenDetector = __esm({
1474
1495
  const visit = this._extractActiveVisit(state);
1475
1496
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
1476
1497
  this._autoScreen = visit.name;
1498
+ this._autoScreenTs = Date.now();
1477
1499
  this._autoVisitSig = this._visitSig(visit);
1478
1500
  this._emitScreenChange(visit.name, "navRef");
1479
1501
  }
@@ -1492,6 +1514,7 @@ var init_AutoScreenDetector = __esm({
1492
1514
  const visit = this._extractActiveVisit(state);
1493
1515
  if (visit) {
1494
1516
  this._autoScreen = visit.name;
1517
+ this._autoScreenTs = Date.now();
1495
1518
  this._autoVisitSig = this._visitSig(visit);
1496
1519
  this._emitScreenChange(visit.name, "navRef");
1497
1520
  }
@@ -1522,6 +1545,7 @@ var init_AutoScreenDetector = __esm({
1522
1545
  const visit = this._extractActiveVisit(state);
1523
1546
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
1524
1547
  this._autoScreen = visit.name;
1548
+ this._autoScreenTs = Date.now();
1525
1549
  this._autoVisitSig = this._visitSig(visit);
1526
1550
  this._emitScreenChange(visit.name, "heuristic");
1527
1551
  }
@@ -1620,7 +1644,7 @@ var init_AutoScreenDetector = __esm({
1620
1644
  if (!reactNav || reactNav.__scalebunAutoHooked) return;
1621
1645
  const detector = this;
1622
1646
  let installed2 = 0;
1623
- const redefine = (key, value) => {
1647
+ const redefine2 = (key, value) => {
1624
1648
  try {
1625
1649
  Object.defineProperty(reactNav, key, {
1626
1650
  value,
@@ -1670,7 +1694,7 @@ var init_AutoScreenDetector = __esm({
1670
1694
  });
1671
1695
  });
1672
1696
  Wrapped.displayName = "ScaleBunNavigationContainer";
1673
- if (redefine("NavigationContainer", Wrapped)) installed2++;
1697
+ if (redefine2("NavigationContainer", Wrapped)) installed2++;
1674
1698
  }
1675
1699
  if (typeof reactNav.createNavigationContainerRef === "function") {
1676
1700
  const originalCreate = reactNav.createNavigationContainerRef;
@@ -1682,7 +1706,7 @@ var init_AutoScreenDetector = __esm({
1682
1706
  }
1683
1707
  return ref;
1684
1708
  };
1685
- if (redefine("createNavigationContainerRef", patchedCreate)) installed2++;
1709
+ if (redefine2("createNavigationContainerRef", patchedCreate)) installed2++;
1686
1710
  }
1687
1711
  if (typeof reactNav.useNavigationContainerRef === "function") {
1688
1712
  const originalHook = reactNav.useNavigationContainerRef;
@@ -1694,7 +1718,7 @@ var init_AutoScreenDetector = __esm({
1694
1718
  }
1695
1719
  return ref;
1696
1720
  };
1697
- if (redefine("useNavigationContainerRef", patchedHook)) installed2++;
1721
+ if (redefine2("useNavigationContainerRef", patchedHook)) installed2++;
1698
1722
  }
1699
1723
  this._autoHookInstalled = installed2 > 0;
1700
1724
  try {
@@ -1743,15 +1767,58 @@ var init_AutoScreenDetector = __esm({
1743
1767
  this._listeners = [];
1744
1768
  this._manualScreen = null;
1745
1769
  this._autoScreen = null;
1770
+ this._autoScreenTs = 0;
1746
1771
  this._navigationRef = null;
1747
1772
  _instance = null;
1748
1773
  }
1749
1774
  /** Get the current best-known screen name */
1750
1775
  getCurrentScreen() {
1751
- if (this._manualScreen && this.isManualScreenActive()) {
1752
- return this._manualScreen;
1776
+ return this._currentScreen().name;
1777
+ }
1778
+ /**
1779
+ * WHERE the current screen name came from, or 'none'.
1780
+ *
1781
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
1782
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
1783
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
1784
+ * causes: the app never instrumented anything, the app instrumented manually and the
1785
+ * value never reached interactions, or auto-detection ran and failed. Those have
1786
+ * different owners and different fixes, and guessing between them is how a P0 stays
1787
+ * open.
1788
+ */
1789
+ getCurrentScreenSource() {
1790
+ return this._currentScreen().source;
1791
+ }
1792
+ /**
1793
+ * LAST WRITER WINS between the manual beacon and auto-detection.
1794
+ *
1795
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
1796
+ * declares a different one — that is what mounting a screen means. It does not
1797
+ * expire, which is the correction here: the previous rule returned the manual name
1798
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
1799
+ * which in a manually-instrumented app is `null`.
1800
+ *
1801
+ * Comparing timestamps rather than hard-coding a priority also handles the real
1802
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
1803
+ * beacon on one modal, and whichever spoke last is the truth.
1804
+ */
1805
+ _currentScreen() {
1806
+ const manual = this._manualScreen;
1807
+ const auto = this._autoScreen;
1808
+ if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
1809
+ return {
1810
+ name: manual,
1811
+ source: "manual"
1812
+ };
1753
1813
  }
1754
- return this._autoScreen;
1814
+ if (auto) return {
1815
+ name: auto,
1816
+ source: "navigation"
1817
+ };
1818
+ return {
1819
+ name: null,
1820
+ source: "none"
1821
+ };
1755
1822
  }
1756
1823
  // ─── Internal ───────────────────────────────────────────────────────
1757
1824
  _emitScreenChange(name, source) {
@@ -3632,17 +3699,54 @@ var init_automaticEvents = __esm({
3632
3699
  function generateInteractionId() {
3633
3700
  return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
3634
3701
  }
3635
- function nearestInteractionStart(starts, occurredAt, toleranceMs = 1500) {
3636
- let best;
3637
- let bestDelta = toleranceMs + 1;
3638
- for (const start of starts) {
3702
+ function correlateInteraction(starts, occurredAt, toleranceMs = CORRELATION_TOLERANCE_MS, marginMs = CORRELATION_MARGIN_MS) {
3703
+ const candidates = [];
3704
+ for (let i = 0; i < starts.length; i++) {
3705
+ const start = starts[i];
3706
+ if (!start) continue;
3639
3707
  const delta = Math.abs(start.occurredAt - occurredAt);
3640
- if (delta < bestDelta) {
3641
- best = start;
3642
- bestDelta = delta;
3708
+ if (delta <= toleranceMs) candidates.push({
3709
+ index: i,
3710
+ delta
3711
+ });
3712
+ }
3713
+ if (candidates.length === 0) {
3714
+ return {
3715
+ index: -1,
3716
+ outcome: "none",
3717
+ deltaMs: null,
3718
+ runnerUpDeltaMs: null
3719
+ };
3720
+ }
3721
+ candidates.sort((a, b) => a.delta - b.delta || a.index - b.index);
3722
+ const best = candidates[0];
3723
+ const runnerUp = candidates[1];
3724
+ if (runnerUp && runnerUp.delta - best.delta < marginMs) {
3725
+ return {
3726
+ index: -1,
3727
+ outcome: "ambiguous",
3728
+ deltaMs: best.delta,
3729
+ runnerUpDeltaMs: runnerUp.delta
3730
+ };
3731
+ }
3732
+ return {
3733
+ start: starts[best.index],
3734
+ index: best.index,
3735
+ outcome: "exact",
3736
+ deltaMs: best.delta,
3737
+ runnerUpDeltaMs: runnerUp ? runnerUp.delta : null
3738
+ };
3739
+ }
3740
+ function pruneInteractionStarts(starts, now2, toleranceMs = CORRELATION_TOLERANCE_MS) {
3741
+ let removed = 0;
3742
+ for (let i = starts.length - 1; i >= 0; i--) {
3743
+ const start = starts[i];
3744
+ if (!start || now2 - start.occurredAt > toleranceMs) {
3745
+ starts.splice(i, 1);
3746
+ removed++;
3643
3747
  }
3644
3748
  }
3645
- return bestDelta <= toleranceMs ? best : void 0;
3749
+ return removed;
3646
3750
  }
3647
3751
  function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3648
3752
  return {
@@ -3660,11 +3764,13 @@ function automaticInteractionProperties(payload, screenName, canonicalMirror) {
3660
3764
  canonical_mirror: canonicalMirror || void 0
3661
3765
  };
3662
3766
  }
3663
- var INTERACTION_PROTOCOL_VERSION;
3767
+ var INTERACTION_PROTOCOL_VERSION, CORRELATION_TOLERANCE_MS, CORRELATION_MARGIN_MS;
3664
3768
  var init_interactionProtocol = __esm({
3665
3769
  "lib/module/features/journey/interactionProtocol.js"() {
3666
3770
  "use strict";
3667
3771
  INTERACTION_PROTOCOL_VERSION = 1;
3772
+ CORRELATION_TOLERANCE_MS = 1500;
3773
+ CORRELATION_MARGIN_MS = 100;
3668
3774
  }
3669
3775
  });
3670
3776
 
@@ -4407,6 +4513,69 @@ var init_SessionManager = __esm({
4407
4513
  state_status: stateStatus,
4408
4514
  ui: details?.ui,
4409
4515
  target_id: details?.targetId,
4516
+ /**
4517
+ * RN-6 — shipped with the row so the join is auditable instead of assumed.
4518
+ *
4519
+ * Without it, an interaction with no `target_id` is ambiguous in the worst way:
4520
+ * it could mean the control genuinely had no identity, or that the correlation
4521
+ * refused and the identity exists but could not be attributed. Those are
4522
+ * different defects with different owners, and the accounting lane cannot give
4523
+ * an honest unplaceable REASON without knowing which.
4524
+ *
4525
+ * Undefined on the JS-fallback path (no native event, so nothing was joined),
4526
+ * which is itself the correct answer there rather than a fabricated 'exact'.
4527
+ */
4528
+ correlation: details?.correlation,
4529
+ correlation_delta_ms: details?.correlationDeltaMs,
4530
+ correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
4531
+ /**
4532
+ * RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
4533
+ * identifier from a structural inference. 'path' means the app authored no testID
4534
+ * and the identity was derived from component names, which is useful but will
4535
+ * change under refactoring; a dashboard that presents the two identically invites
4536
+ * someone to trust a key that is about to move.
4537
+ */
4538
+ target_source: details?.targetSource,
4539
+ /**
4540
+ * RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
4541
+ * 'none' means the app instrumented nothing the SDK could see; 'manual' means a
4542
+ * <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
4543
+ * different owners behind one number that today has none.
4544
+ */
4545
+ screen_source: details?.screenSource,
4546
+ /**
4547
+ * RN-13 — what makes "the heat is on this control" checkable instead of assumed.
4548
+ * Absent when the control could not be measured synchronously, which is a fact worth
4549
+ * storing: an unmeasurable control and a mis-placed tap are different findings.
4550
+ */
4551
+ target_rect: details?.targetRect,
4552
+ /**
4553
+ * RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
4554
+ * table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
4555
+ * these need no migration. Mobile simply never sent them.
4556
+ *
4557
+ * `scroll_source` says whether an offset was available at all: 'none' means the app
4558
+ * uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
4559
+ * the integration and not a tap at the top of a list.
4560
+ */
4561
+ scrollX: details?.scrollX,
4562
+ scrollY: details?.scrollY,
4563
+ scroll_source: details?.scrollSource,
4564
+ /**
4565
+ * RN-4 — what stops a recycled native view merging two different rows into one
4566
+ * hotspot. The key is the app's own keyExtractor output, so it is stable across
4567
+ * scrolling and across sessions; the index is carried so a recycled slot (same index,
4568
+ * different key) can be proven rather than inferred.
4569
+ */
4570
+ item_key: details?.itemKey,
4571
+ item_index: details?.itemIndex,
4572
+ /**
4573
+ * RN-15 — where in the control, not just which control. Turns "they tapped Add to
4574
+ * Cart" into "they tapped its left edge", which is what tells you whether a hit area
4575
+ * is right. Outside 0..1 is a hitSlop tap and is kept as such.
4576
+ */
4577
+ local_u: details?.localU,
4578
+ local_v: details?.localV,
4410
4579
  gestureType,
4411
4580
  x: details?.x,
4412
4581
  y: details?.y,
@@ -5498,10 +5667,10 @@ function requestExport(transport, enabled, presetOrOptions) {
5498
5667
  return;
5499
5668
  }
5500
5669
  const timer = setTimeout(() => {
5501
- const pending4 = _pendingExports.get(requestId);
5502
- if (pending4) {
5670
+ const pending5 = _pendingExports.get(requestId);
5671
+ if (pending5) {
5503
5672
  _pendingExports.delete(requestId);
5504
- pending4.resolve({
5673
+ pending5.resolve({
5505
5674
  success: false,
5506
5675
  error: `Export timed out after ${EXPORT_TIMEOUT_MS / 1e3}s`
5507
5676
  });
@@ -5543,14 +5712,14 @@ function handleExportResult(args) {
5543
5712
  try {
5544
5713
  const requestId = args.requestId;
5545
5714
  if (!requestId) return;
5546
- const pending4 = _pendingExports.get(requestId);
5547
- if (!pending4) {
5715
+ const pending5 = _pendingExports.get(requestId);
5716
+ if (!pending5) {
5548
5717
  logger.warn(`[ExportBridge] Received result for unknown requestId: ${requestId}`);
5549
5718
  return;
5550
5719
  }
5551
- clearTimeout(pending4.timer);
5720
+ clearTimeout(pending5.timer);
5552
5721
  _pendingExports.delete(requestId);
5553
- pending4.resolve({
5722
+ pending5.resolve({
5554
5723
  success: args.success ?? false,
5555
5724
  filePath: args.filePath,
5556
5725
  error: args.error,
@@ -5561,9 +5730,9 @@ function handleExportResult(args) {
5561
5730
  }
5562
5731
  }
5563
5732
  function teardownExports() {
5564
- for (const [, pending4] of _pendingExports) {
5565
- clearTimeout(pending4.timer);
5566
- pending4.reject(new Error("Debug disabled \u2014 export cancelled"));
5733
+ for (const [, pending5] of _pendingExports) {
5734
+ clearTimeout(pending5.timer);
5735
+ pending5.reject(new Error("Debug disabled \u2014 export cancelled"));
5567
5736
  }
5568
5737
  _pendingExports.clear();
5569
5738
  }
@@ -6785,6 +6954,174 @@ var init_installationId = __esm({
6785
6954
  }
6786
6955
  });
6787
6956
 
6957
+ // lib/module/features/journey/scrollContext.js
6958
+ var scrollContext_exports = {};
6959
+ __export(scrollContext_exports, {
6960
+ SCROLL_OFFSET_TTL_MS: () => SCROLL_OFFSET_TTL_MS,
6961
+ clearScrollOffsets: () => clearScrollOffsets,
6962
+ nativeTagOf: () => nativeTagOf,
6963
+ nearestRecordedOffset: () => nearestRecordedOffset,
6964
+ recordScrollOffset: () => recordScrollOffset,
6965
+ trackedScrollerCount: () => trackedScrollerCount
6966
+ });
6967
+ function nativeTagOf(node) {
6968
+ if (!node || typeof node !== "object") return null;
6969
+ const n = node;
6970
+ const tag = typeof n.__nativeTag === "number" ? n.__nativeTag : n._nativeTag;
6971
+ return typeof tag === "number" && Number.isFinite(tag) ? tag : null;
6972
+ }
6973
+ function recordScrollOffset(tag, x, y, now2 = Date.now()) {
6974
+ if (tag === null || !Number.isFinite(x) || !Number.isFinite(y)) return;
6975
+ offsets.set(tag, {
6976
+ x,
6977
+ y,
6978
+ at: now2
6979
+ });
6980
+ if (offsets.size > MAX_TRACKED_SCROLLERS) {
6981
+ let oldestTag = null;
6982
+ let oldestAt = Infinity;
6983
+ for (const [k, v] of offsets) {
6984
+ if (v.at < oldestAt) {
6985
+ oldestAt = v.at;
6986
+ oldestTag = k;
6987
+ }
6988
+ }
6989
+ if (oldestTag !== null) offsets.delete(oldestTag);
6990
+ }
6991
+ }
6992
+ function nearestRecordedOffset(ancestorTags, now2 = Date.now()) {
6993
+ for (const tag of ancestorTags) {
6994
+ if (tag === null) continue;
6995
+ const hit = offsets.get(tag);
6996
+ if (!hit) continue;
6997
+ if (now2 - hit.at > SCROLL_OFFSET_TTL_MS) {
6998
+ offsets.delete(tag);
6999
+ continue;
7000
+ }
7001
+ return {
7002
+ x: hit.x,
7003
+ y: hit.y,
7004
+ tag
7005
+ };
7006
+ }
7007
+ return null;
7008
+ }
7009
+ function clearScrollOffsets() {
7010
+ offsets.clear();
7011
+ }
7012
+ function trackedScrollerCount() {
7013
+ return offsets.size;
7014
+ }
7015
+ var SCROLL_OFFSET_TTL_MS, MAX_TRACKED_SCROLLERS, offsets;
7016
+ var init_scrollContext = __esm({
7017
+ "lib/module/features/journey/scrollContext.js"() {
7018
+ "use strict";
7019
+ SCROLL_OFFSET_TTL_MS = 5 * 6e4;
7020
+ MAX_TRACKED_SCROLLERS = 32;
7021
+ offsets = /* @__PURE__ */ new Map();
7022
+ }
7023
+ });
7024
+
7025
+ // lib/module/features/journey/autoInstrumentScroll.js
7026
+ var autoInstrumentScroll_exports = {};
7027
+ __export(autoInstrumentScroll_exports, {
7028
+ autoInstrumentScrollViews: () => autoInstrumentScrollViews,
7029
+ resetAutoInstrumentForTests: () => resetAutoInstrumentForTests
7030
+ });
7031
+ function redefine(target, key, value) {
7032
+ try {
7033
+ Object.defineProperty(target, key, {
7034
+ configurable: true,
7035
+ enumerable: true,
7036
+ writable: true,
7037
+ value
7038
+ });
7039
+ return target[key] === value;
7040
+ } catch {
7041
+ return false;
7042
+ }
7043
+ }
7044
+ function instrument(Original, React14) {
7045
+ const Wrapped = React14.forwardRef((props, ref) => {
7046
+ const onScroll = (event) => {
7047
+ try {
7048
+ const target = event?.nativeEvent?.target;
7049
+ const offset = event?.nativeEvent?.contentOffset;
7050
+ if (offset) {
7051
+ recordScrollOffset(typeof target === "number" ? target : nativeTagOf(target), offset.x, offset.y);
7052
+ }
7053
+ } catch {
7054
+ }
7055
+ try {
7056
+ props?.onScroll?.(event);
7057
+ } catch (e) {
7058
+ setTimeout(() => {
7059
+ throw e;
7060
+ }, 0);
7061
+ }
7062
+ };
7063
+ const throttle = props?.scrollEventThrottle ?? 16;
7064
+ return React14.createElement(Original, {
7065
+ ...props,
7066
+ scrollEventThrottle: throttle,
7067
+ onScroll,
7068
+ ref
7069
+ });
7070
+ });
7071
+ Wrapped.displayName = "ScaleBunAutoScrollView";
7072
+ return Wrapped;
7073
+ }
7074
+ function autoInstrumentScrollViews() {
7075
+ if (alreadyPatched) return {
7076
+ installed: true
7077
+ };
7078
+ try {
7079
+ const RN = require("react-native");
7080
+ const React14 = require("react");
7081
+ const Original = RN.ScrollView;
7082
+ if (typeof Original !== "function" && typeof Original !== "object") {
7083
+ return {
7084
+ installed: false,
7085
+ reason: "react-native has no ScrollView export to wrap"
7086
+ };
7087
+ }
7088
+ if (Original.__scalebunInstrumented) {
7089
+ alreadyPatched = true;
7090
+ return {
7091
+ installed: true
7092
+ };
7093
+ }
7094
+ const Wrapped = instrument(Original, React14);
7095
+ Wrapped.__scalebunInstrumented = true;
7096
+ if (!redefine(RN, "ScrollView", Wrapped)) {
7097
+ return {
7098
+ installed: false,
7099
+ 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."
7100
+ };
7101
+ }
7102
+ alreadyPatched = true;
7103
+ return {
7104
+ installed: true
7105
+ };
7106
+ } catch (e) {
7107
+ return {
7108
+ installed: false,
7109
+ reason: e?.message ?? "unknown failure"
7110
+ };
7111
+ }
7112
+ }
7113
+ function resetAutoInstrumentForTests() {
7114
+ alreadyPatched = false;
7115
+ }
7116
+ var alreadyPatched;
7117
+ var init_autoInstrumentScroll = __esm({
7118
+ "lib/module/features/journey/autoInstrumentScroll.js"() {
7119
+ "use strict";
7120
+ init_scrollContext();
7121
+ alreadyPatched = false;
7122
+ }
7123
+ });
7124
+
6788
7125
  // lib/module/features/engage/engageSignals.js
6789
7126
  var EngageSignalBus, engageSignals;
6790
7127
  var init_engageSignals = __esm({
@@ -7220,10 +7557,10 @@ async function deliverOtaEvents(params) {
7220
7557
  })
7221
7558
  });
7222
7559
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
7223
- __DEV__ && logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
7560
+ logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
7224
7561
  } catch (err) {
7225
7562
  for (const e of events) otaEventEmitter.emit(e);
7226
- __DEV__ && logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
7563
+ logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
7227
7564
  }
7228
7565
  }
7229
7566
  var import_react_native33, PROGRESS_POST_INTERVAL_MS, DEFAULT_HEALTHY_AFTER_MS, OtaOrchestrator, otaOrchestrator;
@@ -7256,6 +7593,8 @@ var init_OtaOrchestrator = __esm({
7256
7593
  return this.currentBundle;
7257
7594
  }
7258
7595
  isRestartRequiredState = false;
7596
+ /** Root/jailbreak indicators observed at init; empty when none found. */
7597
+ integrityIndicators = [];
7259
7598
  healthyTimer = null;
7260
7599
  bootGuardConfig = {};
7261
7600
  environment = null;
@@ -7290,17 +7629,27 @@ var init_OtaOrchestrator = __esm({
7290
7629
  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.");
7291
7630
  }
7292
7631
  if (this.environment.requiresReactHostIntegration) {
7293
- __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.");
7632
+ 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.");
7294
7633
  }
7295
7634
  this.initialized = true;
7296
7635
  this.enabled = true;
7636
+ try {
7637
+ const raw = NativeScaleBunOta_default?.getDeviceIntegrity?.();
7638
+ this.integrityIndicators = raw ? JSON.parse(raw) : [];
7639
+ if (this.integrityIndicators.length > 0) {
7640
+ 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.`);
7641
+ }
7642
+ } catch {
7643
+ this.integrityIndicators = [];
7644
+ }
7297
7645
  this.bootGuardConfig = config ?? {};
7298
- __DEV__ && logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
7646
+ logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? "unknown"}${this.environment.bridgeless ? ", bridgeless" : ""}${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? "?"}` : ""})`);
7299
7647
  const slotState = this.readSlotState();
7300
7648
  this.hydrateCurrentBundleFromSlots(slotState);
7301
7649
  void prefetchDeviceCountry();
7302
7650
  this.checkBootGuardRecovery(slotState);
7303
7651
  this.verifyRunningBundleIdentity();
7652
+ this.startBootGuardHeartbeat(slotState);
7304
7653
  });
7305
7654
  }
7306
7655
  /**
@@ -7331,13 +7680,13 @@ var init_OtaOrchestrator = __esm({
7331
7680
  releaseId: record4.releaseId ?? void 0,
7332
7681
  sha256: record4.sha256
7333
7682
  };
7334
- __DEV__ && logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
7683
+ logger.debug(`[OTA] Running bundle v${record4.version} (${record4.bundleId})`);
7335
7684
  return;
7336
7685
  }
7337
7686
  this.currentBundle = {
7338
7687
  sha256: current.sha256
7339
7688
  };
7340
- __DEV__ && logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
7689
+ logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}\u2026) with no local install record.`);
7341
7690
  } catch {
7342
7691
  }
7343
7692
  }
@@ -7367,7 +7716,7 @@ var init_OtaOrchestrator = __esm({
7367
7716
  }
7368
7717
  if (!record4.identityToken) return;
7369
7718
  if (running === record4.identityToken) {
7370
- __DEV__ && logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7719
+ logger.debug("[OTA] Install verified \u2014 running bundle matches what was installed.");
7371
7720
  this.retireIdentityToken(record4);
7372
7721
  return;
7373
7722
  }
@@ -7470,6 +7819,13 @@ var init_OtaOrchestrator = __esm({
7470
7819
  isEnabled() {
7471
7820
  return this.enabled;
7472
7821
  }
7822
+ /**
7823
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
7824
+ * Empty means nothing was detected — not proof the device is clean.
7825
+ */
7826
+ deviceIntegrityIndicators() {
7827
+ return [...this.integrityIndicators];
7828
+ }
7473
7829
  isRestartRequired() {
7474
7830
  return this.isRestartRequiredState;
7475
7831
  }
@@ -7563,7 +7919,7 @@ var init_OtaOrchestrator = __esm({
7563
7919
  error: "OTA native module disabled or missing"
7564
7920
  };
7565
7921
  }
7566
- __DEV__ && logger.debug("[OTA] Sync started\u2026");
7922
+ logger.debug("[OTA] Sync started\u2026");
7567
7923
  if (this.currentBundle?.id) {
7568
7924
  otaEventEmitter.emitSimple("CHECK", this.currentBundle.id, {
7569
7925
  releaseId: this.currentBundle.releaseId,
@@ -7572,7 +7928,7 @@ var init_OtaOrchestrator = __esm({
7572
7928
  }
7573
7929
  const checkRes = await this.checkForUpdate(params);
7574
7930
  if (checkRes.action === "NONE") {
7575
- __DEV__ && logger.debug("[OTA] App is up to date");
7931
+ logger.debug("[OTA] App is up to date");
7576
7932
  this.startBootGuardHeartbeat();
7577
7933
  return {
7578
7934
  status: "UP_TO_DATE"
@@ -7615,7 +7971,7 @@ var init_OtaOrchestrator = __esm({
7615
7971
  version: bundle.version
7616
7972
  });
7617
7973
  const downloadStart = Date.now();
7618
- __DEV__ && logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
7974
+ logger.debug(`[OTA] Downloading update v${bundle.version}\u2026`);
7619
7975
  let lastPostAt = 0;
7620
7976
  const postProgress = (bytesRead, status) => {
7621
7977
  const total = bundle.size > 0 ? bundle.size : Math.max(bytesRead, 1);
@@ -7693,7 +8049,7 @@ var init_OtaOrchestrator = __esm({
7693
8049
  staged = true;
7694
8050
  } else {
7695
8051
  if (bundle.patch && NativeScaleBunOta_default.stagePatch) {
7696
- __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`);
8052
+ logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ${(bundle.size / 1024).toFixed(0)} KB full) \u2014 trying patch\u2026`);
7697
8053
  try {
7698
8054
  staged = await NativeScaleBunOta_default.stagePatch(
7699
8055
  bundle.patch.url,
@@ -7703,7 +8059,7 @@ var init_OtaOrchestrator = __esm({
7703
8059
  );
7704
8060
  if (staged) {
7705
8061
  patchUsed = true;
7706
- __DEV__ && logger.debug("[OTA] Patch applied successfully");
8062
+ logger.debug("[OTA] Patch applied successfully");
7707
8063
  } else {
7708
8064
  logger.warn("[OTA] Patch staging returned false \u2014 falling back to full bundle");
7709
8065
  }
@@ -7772,7 +8128,7 @@ var init_OtaOrchestrator = __esm({
7772
8128
  releaseId: bundle.releaseId,
7773
8129
  version: bundle.version
7774
8130
  });
7775
- __DEV__ && logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
8131
+ logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
7776
8132
  await deliverOtaEvents(params);
7777
8133
  const installMode = bundle.installMode ?? "ON_NEXT_RESTART";
7778
8134
  if (params.autoRestart || installMode === "IMMEDIATE") {
@@ -7803,24 +8159,23 @@ var init_OtaOrchestrator = __esm({
7803
8159
  * native boot-guard marker. If JS crashes before the timer fires, the
7804
8160
  * marker stays set and native code reverts on next cold start.
7805
8161
  */
7806
- startBootGuardHeartbeat() {
8162
+ startBootGuardHeartbeat(preRead) {
7807
8163
  if (!NativeScaleBunOta_default) return;
7808
8164
  if (this.healthyTimer) {
7809
8165
  clearTimeout(this.healthyTimer);
7810
8166
  this.healthyTimer = null;
7811
8167
  }
7812
8168
  try {
7813
- const stateJson = NativeScaleBunOta_default.getSlotState();
7814
- const state = JSON.parse(stateJson);
7815
- if (!state.bootMarkerPresent) {
8169
+ const state = preRead !== void 0 ? preRead : JSON.parse(NativeScaleBunOta_default.getSlotState());
8170
+ if (!state?.bootMarkerPresent) {
7816
8171
  return;
7817
8172
  }
7818
8173
  const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
7819
- __DEV__ && logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
8174
+ logger.debug(`[OTA] Boot guard active \u2014 marking healthy after ${healthyMs}ms`);
7820
8175
  this.healthyTimer = setTimeout(() => {
7821
8176
  noThrow(() => {
7822
8177
  NativeScaleBunOta_default?.markHealthy();
7823
- __DEV__ && logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
8178
+ logger.info("[OTA] Boot guard cleared \u2014 bundle marked healthy \u2713");
7824
8179
  this.healthyTimer = null;
7825
8180
  const running = this.currentBundle;
7826
8181
  if (running?.id) {
@@ -7866,11 +8221,11 @@ var init_OtaOrchestrator = __esm({
7866
8221
  } catch {
7867
8222
  }
7868
8223
  this.resumeRestartArmed = false;
7869
- __DEV__ && logger.info("[OTA] App resumed \u2014 activating the staged update now");
8224
+ logger.info("[OTA] App resumed \u2014 activating the staged update now");
7870
8225
  this.restart();
7871
8226
  }
7872
8227
  });
7873
- __DEV__ && logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
8228
+ logger.debug("[OTA] Update staged \u2014 will activate on next resume (ON_NEXT_RESUME)");
7874
8229
  } catch {
7875
8230
  this.resumeRestartArmed = false;
7876
8231
  }
@@ -7887,7 +8242,7 @@ var init_OtaOrchestrator = __esm({
7887
8242
  */
7888
8243
  restart() {
7889
8244
  noThrow(() => {
7890
- __DEV__ && logger.info("[OTA] Triggering app restart\u2026");
8245
+ logger.info("[OTA] Triggering app restart\u2026");
7891
8246
  if (NativeScaleBunOta_default && typeof NativeScaleBunOta_default.restartApp === "function") {
7892
8247
  NativeScaleBunOta_default.restartApp();
7893
8248
  } else if (import_react_native33.DevSettings && typeof import_react_native33.DevSettings.reload === "function") {
@@ -7906,17 +8261,25 @@ var init_OtaOrchestrator = __esm({
7906
8261
  var uiState_exports = {};
7907
8262
  __export(uiState_exports, {
7908
8263
  clearUiState: () => clearUiState,
8264
+ onUiStateChange: () => onUiStateChange,
8265
+ resetUiStateListeners: () => resetUiStateListeners,
7909
8266
  setUiState: () => setUiState,
8267
+ uiStateListenerCount: () => uiStateListenerCount,
7910
8268
  uiStateSignature: () => uiStateSignature
7911
8269
  });
7912
8270
  function setUiState(name, value) {
7913
8271
  const n = clean(name);
7914
8272
  const v = clean(value);
7915
- if (n && v) declared.set(n, v);
8273
+ if (!n || !v) return;
8274
+ if (declared.get(n) === v) return;
8275
+ declared.set(n, v);
8276
+ notifyChanged();
7916
8277
  }
7917
8278
  function clearUiState(name) {
8279
+ const before = declared.size;
7918
8280
  if (name === void 0) declared.clear();
7919
8281
  else declared.delete(clean(name));
8282
+ if (declared.size !== before) notifyChanged();
7920
8283
  }
7921
8284
  function uiStateSignature() {
7922
8285
  if (!declared.size) return void 0;
@@ -7929,12 +8292,42 @@ function uiStateSignature() {
7929
8292
  }
7930
8293
  return signature || void 0;
7931
8294
  }
7932
- var declared, clean;
8295
+ function onUiStateChange(listener) {
8296
+ listeners2.add(listener);
8297
+ return () => {
8298
+ listeners2.delete(listener);
8299
+ };
8300
+ }
8301
+ function resetUiStateListeners() {
8302
+ listeners2.clear();
8303
+ if (pending4 !== null) {
8304
+ clearTimeout(pending4);
8305
+ pending4 = null;
8306
+ }
8307
+ }
8308
+ function uiStateListenerCount() {
8309
+ return listeners2.size;
8310
+ }
8311
+ function notifyChanged() {
8312
+ if (pending4 !== null) return;
8313
+ pending4 = setTimeout(() => {
8314
+ pending4 = null;
8315
+ for (const listener of listeners2) {
8316
+ try {
8317
+ listener();
8318
+ } catch {
8319
+ }
8320
+ }
8321
+ }, 0);
8322
+ }
8323
+ var declared, clean, listeners2, pending4;
7933
8324
  var init_uiState = __esm({
7934
8325
  "lib/module/features/journey/uiState.js"() {
7935
8326
  "use strict";
7936
8327
  declared = /* @__PURE__ */ new Map();
7937
8328
  clean = (s) => typeof s === "string" ? s.replace(/[;:|]/g, "").trim().slice(0, 32) : "";
8329
+ listeners2 = /* @__PURE__ */ new Set();
8330
+ pending4 = null;
7938
8331
  }
7939
8332
  });
7940
8333
 
@@ -9885,6 +10278,22 @@ var SHAPE = {
9885
10278
  }
9886
10279
  },
9887
10280
  captureInteractionHeatmap: bool(true),
10281
+ /**
10282
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
10283
+ * offset, not just ones wrapped in ScaleBunScrollView.
10284
+ *
10285
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
10286
+ * That flag changes what the SDK records about the host; this one changes what the HOST
10287
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
10288
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
10289
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
10290
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
10291
+ * discover afterwards.
10292
+ *
10293
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
10294
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
10295
+ */
10296
+ autoInstrumentScrollViews: bool(false),
9888
10297
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
9889
10298
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
9890
10299
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -11827,8 +12236,8 @@ var PerformanceTransport = class {
11827
12236
  setSink(sink) {
11828
12237
  this.sink = sink;
11829
12238
  if (sink && this.buffer.length > 0) {
11830
- const pending4 = this.buffer.splice(0);
11831
- for (const msg of pending4) {
12239
+ const pending5 = this.buffer.splice(0);
12240
+ for (const msg of pending5) {
11832
12241
  try {
11833
12242
  sink.send(msg);
11834
12243
  } catch {
@@ -13543,22 +13952,22 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
13543
13952
  * lose the snapshot entirely. `imageData.length` is the base64 length, which is within ~1% of the
13544
13953
  * serialized cost and needs no extra pass.
13545
13954
  */
13546
- static _selectFramesToPersist(pending4) {
13955
+ static _selectFramesToPersist(pending5) {
13547
13956
  const isKeyframe = (b) => b.frame.frameType !== "delta";
13548
13957
  const size = (b) => b.imageData?.length ?? 0;
13549
13958
  const keep = [];
13550
13959
  let budget = _BackendSessionAdapter.MAX_PERSIST_BYTES;
13551
13960
  const maxCount = _BackendSessionAdapter.MAX_FRAMES_PER_BATCH;
13552
- for (let i = pending4.length - 1; i >= 0; i--) {
13553
- const b = pending4[i];
13961
+ for (let i = pending5.length - 1; i >= 0; i--) {
13962
+ const b = pending5[i];
13554
13963
  if (!isKeyframe(b)) continue;
13555
13964
  const cost = size(b);
13556
13965
  if (cost > budget || keep.length >= maxCount) continue;
13557
13966
  budget -= cost;
13558
13967
  keep.push(b);
13559
13968
  }
13560
- for (let i = pending4.length - 1; i >= 0; i--) {
13561
- const b = pending4[i];
13969
+ for (let i = pending5.length - 1; i >= 0; i--) {
13970
+ const b = pending5[i];
13562
13971
  if (isKeyframe(b)) continue;
13563
13972
  const cost = size(b);
13564
13973
  if (cost > budget || keep.length >= maxCount) continue;
@@ -13991,6 +14400,10 @@ var SDKBootstrapper = class {
13991
14400
  return null;
13992
14401
  }
13993
14402
  this.config = result.data;
14403
+ const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
14404
+ if (typeof requestedApiBaseUrl === "string" && requestedApiBaseUrl && requestedApiBaseUrl !== DEFAULT_API_BASE_URL) {
14405
+ 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.`);
14406
+ }
13994
14407
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
13995
14408
  if (this.config.logLevel) {
13996
14409
  logger.setLevelByName(this.config.logLevel);
@@ -14155,6 +14568,20 @@ var SDKBootstrapper = class {
14155
14568
  }));
14156
14569
  }
14157
14570
  await this.featureRegistry.initializeAll(featureContext);
14571
+ if (this.config.autoInstrumentScrollViews === true) {
14572
+ try {
14573
+ const {
14574
+ autoInstrumentScrollViews: autoInstrumentScrollViews2
14575
+ } = (init_autoInstrumentScroll(), __toCommonJS(autoInstrumentScroll_exports));
14576
+ const outcome = autoInstrumentScrollViews2();
14577
+ if (!outcome.installed) {
14578
+ logger.warn("[Bootstrap] autoInstrumentScrollViews was requested but could not be installed: " + outcome.reason + " \u2014 taps will carry no content depth.");
14579
+ } else {
14580
+ __DEV__ && logger.debug("[Bootstrap] ScrollView auto-instrumentation installed");
14581
+ }
14582
+ } catch {
14583
+ }
14584
+ }
14158
14585
  if (features.replay !== false) {
14159
14586
  try {
14160
14587
  __DEV__ && logger.debug("[Bootstrap] Initializing unified SessionManager");
@@ -16797,14 +17224,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
16797
17224
  this._apiBaseUrl = result.config.apiBaseUrl;
16798
17225
  this._maybeStartEventTracker(rawConfig);
16799
17226
  if (this._pendingIdentity) {
16800
- const pending4 = this._pendingIdentity;
17227
+ const pending5 = this._pendingIdentity;
16801
17228
  this._pendingIdentity = null;
16802
- this.identify(pending4.userId, pending4.traits);
17229
+ this.identify(pending5.userId, pending5.traits);
16803
17230
  }
16804
17231
  const realQueue = coreContainer.get("PersistentQueue");
16805
17232
  const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
16806
17233
  if (buffered.length > 0) {
16807
- __DEV__ && logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
17234
+ logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
16808
17235
  for (const envelope of buffered) {
16809
17236
  realQueue.add(envelope);
16810
17237
  }
@@ -16895,7 +17322,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
16895
17322
  }
16896
17323
  const clientKey = this._clientKey;
16897
17324
  const apiUrl = this._apiBaseUrl;
16898
- if (!clientKey || !apiUrl) return;
17325
+ if (!clientKey || !apiUrl) {
17326
+ return;
17327
+ }
16899
17328
  this._otaCheckInFlight = true;
16900
17329
  try {
16901
17330
  const info = await bridgeAdapter.getDeviceInfo();
@@ -16946,7 +17375,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
16946
17375
  if (!dbgConfig) return;
16947
17376
  dbgConfig.__skipDeprecationWarning = true;
16948
17377
  this.enableDebug(dbgConfig);
16949
- __DEV__ && logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
17378
+ logger.info("[ScaleBun] Debug auto-connected via init({ debug }).");
16950
17379
  } catch (err) {
16951
17380
  logger.warn("[ScaleBun] Auto debug connection failed:", err?.message);
16952
17381
  }
@@ -16960,7 +17389,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
16960
17389
  const apiBaseUrl = this._apiBaseUrl;
16961
17390
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
16962
17391
  if (enabled && !appId) {
16963
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
17392
+ logger.info("[ScaleBun] Envelope event tracking disabled \u2014 no appId in init config.");
16964
17393
  }
16965
17394
  return;
16966
17395
  }
@@ -17010,7 +17439,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17010
17439
  onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue)
17011
17440
  });
17012
17441
  this._eventTracker.start();
17013
- __DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
17442
+ logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
17014
17443
  this._applyNativeDeviceContext(this._eventTracker);
17015
17444
  this._captureInstallReferrer(this._eventTracker);
17016
17445
  const tracker = this._eventTracker;
@@ -17644,7 +18073,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17644
18073
  const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
17645
18074
  const messages = cfg?.inAppMessages ?? [];
17646
18075
  const previews = messages.filter((m) => m.preview === true || m.isTest === true);
17647
- __DEV__ && logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
18076
+ logger.info(`[Engage] debugFetchInAppMessages \u2192 ${messages.length} message(s), ${previews.length} test preview(s)`);
17648
18077
  return messages;
17649
18078
  },
17650
18079
  /** Upload a survey/NPS/feedback response (idempotent, rides the outbox). */
@@ -17920,7 +18349,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
17920
18349
  logger.warn("SDK not initialized. Event dropped:", name);
17921
18350
  return;
17922
18351
  }
17923
- __DEV__ && logger.debug("Tracking event:", name);
18352
+ logger.debug("Tracking event:", name);
17924
18353
  engageSignals.emit(name, payload);
17925
18354
  const adapter = SessionManager.getExistingInstance()?.getBackendTransport();
17926
18355
  if (!isUploadedSessionEvent(name)) {
@@ -18049,7 +18478,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18049
18478
  await Promise.all([this._eventTracker?.flush() ?? Promise.resolve(), adapter?.flushAnalytics() ?? Promise.resolve()]);
18050
18479
  return;
18051
18480
  }
18052
- __DEV__ && logger.debug("Flushing events from persistent queue...");
18481
+ logger.debug("Flushing events from persistent queue...");
18053
18482
  const httpClient = coreContainer.get("HttpClient");
18054
18483
  await flushQueue(this._getQueue(), httpClient, getDeviceId());
18055
18484
  });
@@ -18071,9 +18500,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
18071
18500
  const bootstrapPerfFeature = getPerformanceFeature();
18072
18501
  if (bootstrapPerfFeature) {
18073
18502
  this._performanceFeature = bootstrapPerfFeature;
18074
- __DEV__ && logger.debug("Performance feature linked from bootstrap");
18503
+ logger.debug("Performance feature linked from bootstrap");
18075
18504
  }
18076
- __DEV__ && logger.info("Debug enabled");
18505
+ logger.info("Debug enabled");
18077
18506
  });
18078
18507
  }
18079
18508
  /** Disable the debug subsystem and clean up */
@@ -18216,14 +18645,14 @@ var ScaleBunFacade = class _ScaleBunFacade {
18216
18645
  return false;
18217
18646
  }
18218
18647
  sm.startSession(metadata);
18219
- __DEV__ && logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
18648
+ logger.info("session.start() \u2705 sessionId:", sm.currentSessionId);
18220
18649
  return true;
18221
18650
  },
18222
18651
  end: (reason) => {
18223
18652
  const sm = SessionManager.getExistingInstance();
18224
18653
  if (!sm || !sm.isActive) return false;
18225
18654
  sm.endSession(reason);
18226
- __DEV__ && logger.info("session.end() \u2705");
18655
+ logger.info("session.end() \u2705");
18227
18656
  return true;
18228
18657
  },
18229
18658
  emitEvent: (type, data) => {
@@ -18276,7 +18705,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18276
18705
  ...this._privacyConfig,
18277
18706
  ...config
18278
18707
  };
18279
- __DEV__ && logger.debug("[report] Privacy config updated");
18708
+ logger.debug("[report] Privacy config updated");
18280
18709
  },
18281
18710
  /** Set branding for exported reports. */
18282
18711
  setBranding: (config) => {
@@ -18284,7 +18713,7 @@ var ScaleBunFacade = class _ScaleBunFacade {
18284
18713
  ...this._brandingConfig,
18285
18714
  ...config
18286
18715
  };
18287
- __DEV__ && logger.debug("[report] Branding config updated");
18716
+ logger.debug("[report] Branding config updated");
18288
18717
  },
18289
18718
  /** Get current privacy config. */
18290
18719
  getPrivacy: () => ({
@@ -18331,9 +18760,9 @@ var ScaleBunFacade = class _ScaleBunFacade {
18331
18760
  AutoScreenDetector: AutoScreenDetector2
18332
18761
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
18333
18762
  AutoScreenDetector2.getInstance().setNavigationRef(ref);
18334
- __DEV__ && logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
18763
+ logger.info("[ScaleBun] Navigation ref set \u2192 auto screen detection active");
18335
18764
  } catch (err) {
18336
- __DEV__ && logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
18765
+ logger.debug("[ScaleBun] Navigation ref stashed (detector not ready):", err?.message);
18337
18766
  }
18338
18767
  });
18339
18768
  }
@@ -18640,8 +19069,8 @@ function installEngageTriggerEngine(deps) {
18640
19069
  if (!cachedConfig) return;
18641
19070
  configReady = true;
18642
19071
  const now2 = Date.now();
18643
- const pending4 = recent.splice(0);
18644
- for (const e of pending4) {
19072
+ const pending5 = recent.splice(0);
19073
+ for (const e of pending5) {
18645
19074
  const age = now2 - e.at;
18646
19075
  if (age > RECENT_TTL_MS) continue;
18647
19076
  const remaining = delayMsForTrigger(cachedConfig, e.options.trigger) - age;
@@ -19906,11 +20335,11 @@ var keyOf = (value) => value.trim();
19906
20335
  var validRect = (rect) => [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
19907
20336
  function createEngageAnchorRegistry() {
19908
20337
  const entries = /* @__PURE__ */ new Map();
19909
- const listeners2 = /* @__PURE__ */ new Map();
20338
+ const listeners3 = /* @__PURE__ */ new Map();
19910
20339
  const notify = (rawKey) => {
19911
20340
  const key = keyOf(rawKey);
19912
20341
  if (!key) return;
19913
- listeners2.get(key)?.forEach((listener) => listener());
20342
+ listeners3.get(key)?.forEach((listener) => listener());
19914
20343
  };
19915
20344
  return {
19916
20345
  register(rawKey, node) {
@@ -19952,12 +20381,12 @@ function createEngageAnchorRegistry() {
19952
20381
  const key = keyOf(rawKey);
19953
20382
  if (!key) return () => {
19954
20383
  };
19955
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
20384
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
19956
20385
  set.add(listener);
19957
- listeners2.set(key, set);
20386
+ listeners3.set(key, set);
19958
20387
  return () => {
19959
20388
  set.delete(listener);
19960
- if (set.size === 0) listeners2.delete(key);
20389
+ if (set.size === 0) listeners3.delete(key);
19961
20390
  };
19962
20391
  },
19963
20392
  notify
@@ -29066,10 +29495,10 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
29066
29495
  var keyOf2 = (value) => value.trim();
29067
29496
  function createEngageInlinePlacementRegistry() {
29068
29497
  const slots = /* @__PURE__ */ new Map();
29069
- const pending4 = /* @__PURE__ */ new Map();
29070
- const listeners2 = /* @__PURE__ */ new Map();
29498
+ const pending5 = /* @__PURE__ */ new Map();
29499
+ const listeners3 = /* @__PURE__ */ new Map();
29071
29500
  const notify = (key) => {
29072
- listeners2.get(key)?.forEach((listener) => listener());
29501
+ listeners3.get(key)?.forEach((listener) => listener());
29073
29502
  };
29074
29503
  return {
29075
29504
  register(rawKey, sink) {
@@ -29086,7 +29515,7 @@ function createEngageInlinePlacementRegistry() {
29086
29515
  sink,
29087
29516
  token
29088
29517
  });
29089
- sink(pending4.get(key) ?? null);
29518
+ sink(pending5.get(key) ?? null);
29090
29519
  if (!previous) notify(key);
29091
29520
  return () => {
29092
29521
  if (slots.get(key)?.token !== token) return;
@@ -29097,7 +29526,7 @@ function createEngageInlinePlacementRegistry() {
29097
29526
  deliver(rawKey, delivery) {
29098
29527
  const key = keyOf2(rawKey);
29099
29528
  if (!key) return false;
29100
- pending4.set(key, delivery);
29529
+ pending5.set(key, delivery);
29101
29530
  const slot = slots.get(key);
29102
29531
  slot?.sink(delivery);
29103
29532
  return slot !== void 0;
@@ -29105,9 +29534,9 @@ function createEngageInlinePlacementRegistry() {
29105
29534
  clear(rawKey, messageId) {
29106
29535
  const key = keyOf2(rawKey);
29107
29536
  if (!key) return;
29108
- const current = pending4.get(key);
29537
+ const current = pending5.get(key);
29109
29538
  if (!current || messageId !== void 0 && current.message?.id !== messageId) return;
29110
- pending4.delete(key);
29539
+ pending5.delete(key);
29111
29540
  slots.get(key)?.sink(null);
29112
29541
  },
29113
29542
  has(rawKey) {
@@ -29118,12 +29547,12 @@ function createEngageInlinePlacementRegistry() {
29118
29547
  const key = keyOf2(rawKey);
29119
29548
  if (!key) return () => {
29120
29549
  };
29121
- const set = listeners2.get(key) ?? /* @__PURE__ */ new Set();
29550
+ const set = listeners3.get(key) ?? /* @__PURE__ */ new Set();
29122
29551
  set.add(listener);
29123
- listeners2.set(key, set);
29552
+ listeners3.set(key, set);
29124
29553
  return () => {
29125
29554
  set.delete(listener);
29126
- if (set.size === 0) listeners2.delete(key);
29555
+ if (set.size === 0) listeners3.delete(key);
29127
29556
  };
29128
29557
  }
29129
29558
  };
@@ -29912,6 +30341,7 @@ var import_react_native42 = require("react-native");
29912
30341
  // lib/module/features/journey/touchTarget.js
29913
30342
  var MAX_DEPTH = 12;
29914
30343
  var MAX_LEN = 60;
30344
+ var MAX_PATH_SEGMENTS = 4;
29915
30345
  function str2(v) {
29916
30346
  if (typeof v !== "string") return void 0;
29917
30347
  const s = v.trim();
@@ -29943,41 +30373,136 @@ function fiberOf(event) {
29943
30373
  const e = event;
29944
30374
  const direct = e._targetInst;
29945
30375
  if (direct && typeof direct === "object") return direct;
29946
- const target = e.target;
29947
- if (target && typeof target === "object") {
29948
- const handle = target._internalFiberInstanceHandleDEV ?? target._internalInstanceHandle;
30376
+ const target = e.target ?? e.nativeEvent?.target;
30377
+ if (target) {
30378
+ const handle = target._internalInstanceHandle ?? target._targetInst;
29949
30379
  if (handle && typeof handle === "object") return handle;
29950
30380
  }
29951
30381
  return null;
29952
30382
  }
29953
- function resolveTouchTarget(event, maxDepth = MAX_DEPTH) {
30383
+ function resolveTouchTarget(event, optsOrMaxDepth = {}) {
30384
+ const opts = typeof optsOrMaxDepth === "number" ? {
30385
+ maxDepth: optsOrMaxDepth
30386
+ } : optsOrMaxDepth;
30387
+ const maxDepth = opts.maxDepth ?? MAX_DEPTH;
29954
30388
  let f = fiberOf(event);
29955
30389
  if (!f) return null;
29956
30390
  const out = {};
30391
+ const segments = [];
30392
+ const ancestors = [];
29957
30393
  let depth = 0;
29958
30394
  try {
29959
30395
  while (f && depth < maxDepth) {
29960
30396
  const props = f.memoizedProps;
29961
30397
  if (props) {
29962
- out.accessibilityLabel ??= str2(props.accessibilityLabel);
30398
+ const beforeTestId = out.testID;
29963
30399
  out.testID ??= str2(props.testID);
29964
- out.text ??= textChild(props);
30400
+ if (!beforeTestId && out.testID) out.node = f.stateNode;
30401
+ if (typeof props.cellKey === "string" && props.cellKey) {
30402
+ out.itemKey ??= str2(props.cellKey);
30403
+ if (typeof props.index === "number" && Number.isFinite(props.index)) {
30404
+ out.itemIndex ??= props.index;
30405
+ }
30406
+ }
30407
+ if (opts.includeText) {
30408
+ out.accessibilityLabel ??= str2(props.accessibilityLabel);
30409
+ out.text ??= textChild(props);
30410
+ }
30411
+ }
30412
+ if (f.stateNode) ancestors.push(f.stateNode);
30413
+ const name = componentName(f);
30414
+ if (name) {
30415
+ out.component ??= name;
30416
+ if (segments[segments.length - 1] !== name && segments.length < MAX_PATH_SEGMENTS) {
30417
+ segments.push(name);
30418
+ }
29965
30419
  }
29966
- out.component ??= componentName(f);
29967
- if (out.accessibilityLabel && out.testID) break;
30420
+ if (out.testID && segments.length >= MAX_PATH_SEGMENTS) break;
29968
30421
  f = f.return ?? null;
29969
30422
  depth++;
29970
30423
  }
29971
30424
  } catch {
29972
30425
  }
29973
- return out.accessibilityLabel || out.testID || out.text || out.component ? out : null;
30426
+ if (segments.length) out.path = segments.join("<");
30427
+ if (!out.node) out.node = fiberOf(event)?.stateNode;
30428
+ if (ancestors.length) out.ancestors = ancestors;
30429
+ return out.testID || out.path || out.component || out.accessibilityLabel || out.text || out.itemKey ? out : null;
29974
30430
  }
29975
30431
  function describeTouchTarget(t) {
29976
30432
  if (!t) return void 0;
29977
- return t.accessibilityLabel || t.testID || t.text || t.component;
30433
+ return t.testID || t.path || t.component;
30434
+ }
30435
+ function targetIdentity(t) {
30436
+ if (!t) return {
30437
+ source: "none"
30438
+ };
30439
+ if (t.testID) return {
30440
+ id: t.testID,
30441
+ source: "testID"
30442
+ };
30443
+ if (t.path) return {
30444
+ id: t.path,
30445
+ source: "path"
30446
+ };
30447
+ if (t.component) return {
30448
+ id: t.component,
30449
+ source: "component"
30450
+ };
30451
+ return {
30452
+ source: "none"
30453
+ };
30454
+ }
30455
+
30456
+ // lib/module/features/journey/targetGeometry.js
30457
+ function readRectSync(node) {
30458
+ if (!node || typeof node !== "object") return null;
30459
+ const n = node;
30460
+ const fn = typeof n.unstable_getBoundingClientRect === "function" ? n.unstable_getBoundingClientRect : typeof n.getBoundingClientRect === "function" ? n.getBoundingClientRect : null;
30461
+ if (!fn) return null;
30462
+ try {
30463
+ const r = fn.call(n);
30464
+ if (!r) return null;
30465
+ const {
30466
+ x,
30467
+ y,
30468
+ width,
30469
+ height
30470
+ } = r;
30471
+ if (![x, y, width, height].every((v) => typeof v === "number" && Number.isFinite(v))) return null;
30472
+ if (width <= 0 || height <= 0) return null;
30473
+ return {
30474
+ x,
30475
+ y,
30476
+ width,
30477
+ height
30478
+ };
30479
+ } catch {
30480
+ return null;
30481
+ }
30482
+ }
30483
+ function normalizeRect(rect, base) {
30484
+ if (!rect || !base || base.w <= 0 || base.h <= 0) return null;
30485
+ const out = {
30486
+ x: rect.x / base.w,
30487
+ y: rect.y / base.h,
30488
+ width: rect.width / base.w,
30489
+ height: rect.height / base.h
30490
+ };
30491
+ if (![out.x, out.y, out.width, out.height].every((v) => Number.isFinite(v))) return null;
30492
+ return out;
30493
+ }
30494
+ function localPoint(rect, nx, ny) {
30495
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
30496
+ if (typeof nx !== "number" || typeof ny !== "number") return null;
30497
+ if (!Number.isFinite(nx) || !Number.isFinite(ny)) return null;
30498
+ return {
30499
+ u: (nx - rect.x) / rect.width,
30500
+ v: (ny - rect.y) / rect.height
30501
+ };
29978
30502
  }
29979
30503
 
29980
30504
  // lib/module/features/journey/ScaleBunDebugRoot.js
30505
+ init_scrollContext();
29981
30506
  init_device();
29982
30507
  init_interactionProtocol();
29983
30508
  var import_jsx_runtime8 = require("react/jsx-runtime");
@@ -29989,6 +30514,7 @@ function getBootstrap() {
29989
30514
  return _bootstrap;
29990
30515
  }
29991
30516
  var JourneyContext = /* @__PURE__ */ (0, import_react9.createContext)(null);
30517
+ var UI_STATE_CAPTURE_MIN_INTERVAL_MS = 1e3;
29992
30518
  function ScaleBunDebugRoot({
29993
30519
  children,
29994
30520
  navigationRef,
@@ -30167,6 +30693,28 @@ function ScaleBunDebugRoot({
30167
30693
  clearTimeout(timer);
30168
30694
  };
30169
30695
  }, [navigationRef]);
30696
+ (0, import_react9.useEffect)(() => {
30697
+ let lastCaptureTs = 0;
30698
+ const {
30699
+ onUiStateChange: onUiStateChange2
30700
+ } = (init_uiState(), __toCommonJS(uiState_exports));
30701
+ const unsubscribe = onUiStateChange2(() => {
30702
+ const now2 = Date.now();
30703
+ if (now2 - lastCaptureTs < UI_STATE_CAPTURE_MIN_INTERVAL_MS) return;
30704
+ lastCaptureTs = now2;
30705
+ try {
30706
+ const {
30707
+ SessionManager: SessionManager2
30708
+ } = (init_SessionManager(), __toCommonJS(SessionManager_exports));
30709
+ const sm = SessionManager2.getExistingInstance();
30710
+ if (sm && typeof sm.captureManualFrame === "function") {
30711
+ void sm.captureManualFrame();
30712
+ }
30713
+ } catch {
30714
+ }
30715
+ });
30716
+ return unsubscribe;
30717
+ }, []);
30170
30718
  const lastNativeTouchTsRef = (0, import_react9.useRef)(0);
30171
30719
  const nativeTrackingConfirmedRef = (0, import_react9.useRef)(false);
30172
30720
  const touchStartRef = (0, import_react9.useRef)(null);
@@ -30183,10 +30731,15 @@ function ScaleBunDebugRoot({
30183
30731
  lastNativeTouchTsRef.current = Date.now();
30184
30732
  nativeTrackingConfirmedRef.current = true;
30185
30733
  const nativeOccurredAt = typeof event.occurredAt === "number" ? event.occurredAt : (typeof event.timestamp === "number" ? event.timestamp : Date.now()) - (typeof event.durationMs === "number" ? event.durationMs : 0);
30186
- const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
30734
+ pruneInteractionStarts(interactionStartsRef.current, nativeOccurredAt);
30735
+ const correlation = correlateInteraction(interactionStartsRef.current, nativeOccurredAt);
30736
+ const start = correlation.start;
30737
+ if (correlation.index >= 0) {
30738
+ interactionStartsRef.current.splice(correlation.index, 1);
30739
+ }
30187
30740
  if (start) {
30188
- const pending4 = pendingJsEmitsRef.current.get(start.interactionId);
30189
- if (pending4 !== void 0) clearTimeout(pending4);
30741
+ const pending5 = pendingJsEmitsRef.current.get(start.interactionId);
30742
+ if (pending5 !== void 0) clearTimeout(pending5);
30190
30743
  pendingJsEmitsRef.current.delete(start.interactionId);
30191
30744
  }
30192
30745
  const {
@@ -30209,11 +30762,62 @@ function ScaleBunDebugRoot({
30209
30762
  interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
30210
30763
  interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
30211
30764
  occurredAt: nativeOccurredAt,
30765
+ /**
30766
+ * RN-6 — THESE FIVE FIELDS ARE THE ONLY ONES THE JOIN CARRIES, AND
30767
+ * THEREFORE THE ONLY ONES A BAD JOIN CAN FALSIFY.
30768
+ *
30769
+ * Coordinates below come from native and are right regardless. So a
30770
+ * mispairing does not produce visibly broken data: it produces heat
30771
+ * in the correct position attributed to the wrong control, in the
30772
+ * wrong UI state, on the wrong screen. Every field is individually
30773
+ * well-formed and nothing downstream can tell.
30774
+ *
30775
+ * When the correlation is not 'exact' they are left UNDEFINED rather
30776
+ * than filled from a best guess. An interaction with no target is
30777
+ * honest and visibly incomplete — the accounting lane can count it as
30778
+ * unplaceable and say why. An interaction with the WRONG target is
30779
+ * indistinguishable from a correct one and silently corrupts every
30780
+ * aggregate it enters. `null` is not `0`, and a guess is not a
30781
+ * measurement.
30782
+ */
30212
30783
  ui: start?.ui,
30213
30784
  stateStatus: start?.stateStatus ?? "not_captured",
30214
30785
  target: start?.target,
30215
30786
  targetId: start?.targetId,
30787
+ targetSource: start?.targetSource,
30216
30788
  screenName: start?.screenName,
30789
+ screenSource: start?.screenSource ?? "none",
30790
+ targetRect: start?.targetRect,
30791
+ /**
30792
+ * RN-15 — WHERE IN THE CONTROL, computed where both halves exist: the
30793
+ * point comes from the native event and the rect was measured at
30794
+ * finger-down, in the same capture base. Pairing them here rather than
30795
+ * downstream stops anything later reaching for a different rect.
30796
+ */
30797
+ ...(() => {
30798
+ const lp = localPoint(start?.targetRect ?? null, event.normalizedX, event.normalizedY);
30799
+ return lp ? {
30800
+ localU: lp.u,
30801
+ localV: lp.v
30802
+ } : {};
30803
+ })(),
30804
+ scrollX: start?.scrollX,
30805
+ scrollY: start?.scrollY,
30806
+ scrollSource: start?.scrollSource ?? "none",
30807
+ itemKey: start?.itemKey,
30808
+ itemIndex: start?.itemIndex,
30809
+ /**
30810
+ * The join's own verdict, shipped with the row so it is auditable
30811
+ * rather than a claim made in a comment. 'ambiguous' and 'none' are
30812
+ * deliberately distinguished: 'none' means the JS handler never fired
30813
+ * (a real capture gap on this platform), 'ambiguous' means it fired
30814
+ * more than once too close together to attribute. Those have
30815
+ * different fixes, and collapsing them would hide which one is
30816
+ * actually happening in production.
30817
+ */
30818
+ correlation: correlation.outcome,
30819
+ correlationDeltaMs: correlation.deltaMs,
30820
+ correlationRunnerUpDeltaMs: correlation.runnerUpDeltaMs,
30217
30821
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30218
30822
  x: event.rawX,
30219
30823
  y: event.rawY,
@@ -30285,6 +30889,7 @@ function ScaleBunDebugRoot({
30285
30889
  const target = resolveTouchTarget(e);
30286
30890
  let ui;
30287
30891
  let screenName;
30892
+ let screenSource = "none";
30288
30893
  try {
30289
30894
  const {
30290
30895
  uiStateSignature: uiStateSignature2
@@ -30296,7 +30901,9 @@ function ScaleBunDebugRoot({
30296
30901
  const {
30297
30902
  AutoScreenDetector: AutoScreenDetector2
30298
30903
  } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
30299
- screenName = AutoScreenDetector2.getInstance().getCurrentScreen() || void 0;
30904
+ const detector = AutoScreenDetector2.getInstance();
30905
+ screenName = detector.getCurrentScreen() || void 0;
30906
+ screenSource = screenName ? detector.getCurrentScreenSource() : "none";
30300
30907
  } catch {
30301
30908
  }
30302
30909
  const start = {
@@ -30306,9 +30913,91 @@ function ScaleBunDebugRoot({
30306
30913
  y: touch.pageY,
30307
30914
  locationX: touch.locationX,
30308
30915
  locationY: touch.locationY,
30916
+ /**
30917
+ * RN-1 + RN-2 — the identity that is persisted, and it is now code-derived.
30918
+ *
30919
+ * `describeTouchTarget` returns `testID` -> structural path -> component name,
30920
+ * and never a caption or an accessibilityLabel. Before RN-2 it preferred free
30921
+ * text, so this line was writing user data ("Pay EGP 1,442", an order row
30922
+ * labelled with an email address, a contact's name) into the analytics store —
30923
+ * and, because `target` is an aggregation key, splitting one button into one
30924
+ * bucket per person at the same time.
30925
+ *
30926
+ * `targetId` used to be `target?.testID` alone, which is `undefined` in the
30927
+ * overwhelming majority of apps — no testIDs authored — so the field the
30928
+ * backend keys on was empty almost always. It now falls back to the structural
30929
+ * path, with `targetSource` recording WHICH it got.
30930
+ */
30309
30931
  target: describeTouchTarget(target),
30310
- targetId: target?.testID,
30932
+ targetId: targetIdentity(target).id,
30933
+ /**
30934
+ * WHICH KIND of identity this is, because they are not equally trustworthy: a
30935
+ * `testID` is a promise from the app's authors that this control is this
30936
+ * control; a `path` is an inference that breaks when the tree is refactored.
30937
+ * Without this the accounting lane cannot distinguish "the control had no
30938
+ * identity" from "the identity is a structural guess", and both would render
30939
+ * as an equally confident target.
30940
+ */
30941
+ targetSource: targetIdentity(target).source,
30942
+ /* RN-4 — row identity from the app's own keyExtractor; see touchTarget.ts. */
30943
+ itemKey: target?.itemKey,
30944
+ itemIndex: target?.itemIndex,
30945
+ /**
30946
+ * RN-13 — the control's rectangle, so `heat ∈ target` is decidable.
30947
+ *
30948
+ * Read SYNCHRONOUSLY here, in the same tick as the touch, and normalized against
30949
+ * the SAME measured root the tap's own coordinates use. Both halves matter:
30950
+ *
30951
+ * - A callback measurement (`measureInWindow`) would resolve after this handler
30952
+ * returns, so the rect attached to a tap could be the previous tap's, with
30953
+ * nothing afterwards able to tell. `targetGeometry` refuses to guess: it reads
30954
+ * now or reports null.
30955
+ * - Normalizing against a window size instead of the root view is off by the
30956
+ * status bar on most devices, which would read as "the heat is just outside
30957
+ * the button" everywhere rather than as a measurement bug.
30958
+ *
30959
+ * `measureRoot()` is called first because the cached root rect is populated
30960
+ * asynchronously at mount; without it the first taps of a session normalize
30961
+ * against nothing and get a null rect for a reason that has nothing to do with
30962
+ * the control.
30963
+ */
30964
+ /**
30965
+ * RN-3 — WHERE IN THE CONTENT, not just where on the glass.
30966
+ *
30967
+ * Two taps at the same screen coordinate one viewport apart are two different
30968
+ * controls. Without an offset they merge into one hotspot, and the heatmap shows
30969
+ * heavy engagement at a position where, in content terms, nothing in particular
30970
+ * is. The offset is also a CACHE dimension — two scroll states of one route must
30971
+ * not share a backdrop — so its absence corrupts the picture as well as the count.
30972
+ *
30973
+ * `ancestors` is innermost-first, so this takes the NEAREST scroller that has a
30974
+ * recorded position: a tap inside a horizontal carousel belongs to the carousel's
30975
+ * content, not to how far the page behind it had scrolled.
30976
+ *
30977
+ * Null when no ancestor has one — never zero. Zero is a real scroll position (the
30978
+ * top of a list), and reporting it for "unknown" would place every unmeasured tap
30979
+ * at the top of its content, indistinguishable from a genuine tap there.
30980
+ */
30981
+ ...(() => {
30982
+ const hit = nearestRecordedOffset((target?.ancestors ?? []).map((n) => nativeTagOf(n)));
30983
+ return hit ? {
30984
+ scrollX: hit.x,
30985
+ scrollY: hit.y,
30986
+ scrollSource: "wrapper"
30987
+ } : {
30988
+ scrollSource: "none"
30989
+ };
30990
+ })(),
30991
+ targetRect: (() => {
30992
+ measureRoot();
30993
+ const root = rootRectRef.current;
30994
+ return normalizeRect(readRectSync(target?.node), root ? {
30995
+ w: root.w,
30996
+ h: root.h
30997
+ } : null) ?? void 0;
30998
+ })(),
30311
30999
  screenName,
31000
+ screenSource,
30312
31001
  ui,
30313
31002
  stateStatus: ui ? "captured_nonempty" : "not_instrumented",
30314
31003
  emitAutomaticAnalytics: captureAutomaticInteractions
@@ -30345,7 +31034,12 @@ function ScaleBunDebugRoot({
30345
31034
  }
30346
31035
  measureRoot();
30347
31036
  const rootRect = rootRectRef.current;
30348
- const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
31037
+ const freshTarget = start ? null : resolveTouchTarget(e);
31038
+ const tapped = start?.target ?? describeTouchTarget(freshTarget);
31039
+ const identity = start ? {
31040
+ id: start.targetId,
31041
+ source: start.targetSource
31042
+ } : targetIdentity(freshTarget);
30349
31043
  const gestureDetails = {
30350
31044
  target: tapped,
30351
31045
  interactionId: start?.interactionId ?? generateInteractionId(),
@@ -30353,8 +31047,16 @@ function ScaleBunDebugRoot({
30353
31047
  occurredAt: start?.occurredAt ?? Date.now(),
30354
31048
  ui: start?.ui,
30355
31049
  stateStatus: start?.stateStatus ?? "not_captured",
30356
- targetId: start?.targetId,
31050
+ targetId: identity.id,
31051
+ targetSource: identity.source,
30357
31052
  screenName: start?.screenName,
31053
+ screenSource: start?.screenSource ?? "none",
31054
+ targetRect: start?.targetRect,
31055
+ scrollX: start?.scrollX,
31056
+ scrollY: start?.scrollY,
31057
+ scrollSource: start?.scrollSource ?? "none",
31058
+ itemKey: start?.itemKey,
31059
+ itemIndex: start?.itemIndex,
30358
31060
  emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
30359
31061
  x: start?.x ?? touch.pageX,
30360
31062
  y: start?.y ?? touch.pageY,
@@ -30370,6 +31072,11 @@ function ScaleBunDebugRoot({
30370
31072
  const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
30371
31073
  gestureDetails.normalizedX = nx;
30372
31074
  gestureDetails.normalizedY = ny;
31075
+ const lp = localPoint(start?.targetRect ?? null, nx, ny);
31076
+ if (lp) {
31077
+ gestureDetails.localU = lp.u;
31078
+ gestureDetails.localV = lp.v;
31079
+ }
30373
31080
  gestureDetails.normalizedPrecomputed = true;
30374
31081
  gestureDetails.viewportWidth = rootRect.w;
30375
31082
  gestureDetails.viewportHeight = rootRect.h;
@@ -30498,6 +31205,13 @@ function useScaleBunScreen(name) {
30498
31205
  }
30499
31206
  } catch {
30500
31207
  }
31208
+ try {
31209
+ const {
31210
+ AutoScreenDetector: AutoScreenDetector2
31211
+ } = (init_AutoScreenDetector(), __toCommonJS(AutoScreenDetector_exports));
31212
+ AutoScreenDetector2.getInstance().setManualScreen(name);
31213
+ } catch {
31214
+ }
30501
31215
  try {
30502
31216
  const bs = getBootstrap();
30503
31217
  const perfFeature = bs?.getPerformanceFeature?.();
@@ -30648,6 +31362,18 @@ var ScaleBunScrollView = /* @__PURE__ */ import_react12.default.forwardRef(funct
30648
31362
  }
30649
31363
  } catch {
30650
31364
  }
31365
+ try {
31366
+ const {
31367
+ recordScrollOffset: recordScrollOffset2,
31368
+ nativeTagOf: nativeTagOf2
31369
+ } = (init_scrollContext(), __toCommonJS(scrollContext_exports));
31370
+ const {
31371
+ contentOffset,
31372
+ target
31373
+ } = event.nativeEvent;
31374
+ recordScrollOffset2(typeof target === "number" ? target : nativeTagOf2(target), contentOffset.x, contentOffset.y);
31375
+ } catch {
31376
+ }
30651
31377
  if (userOnScroll) {
30652
31378
  userOnScroll(event);
30653
31379
  }