@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
@@ -119,12 +119,12 @@ async function deliverOtaEvents(params) {
119
119
  })
120
120
  });
121
121
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
122
- __DEV__ && _internalLogger.logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
122
+ _internalLogger.logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
123
123
  } catch (err) {
124
124
  // Failed delivery must not fail the update — but the events must not be
125
125
  // lost either: requeue so the next sync retries them.
126
126
  for (const e of events) _OtaEventEmitter.otaEventEmitter.emit(e);
127
- __DEV__ && _internalLogger.logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
127
+ _internalLogger.logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
128
128
  }
129
129
  }
130
130
 
@@ -144,6 +144,8 @@ class OtaOrchestrator {
144
144
  return this.currentBundle;
145
145
  }
146
146
  isRestartRequiredState = false;
147
+ /** Root/jailbreak indicators observed at init; empty when none found. */
148
+ integrityIndicators = [];
147
149
  healthyTimer = null;
148
150
  bootGuardConfig = {};
149
151
  environment = null;
@@ -199,12 +201,30 @@ class OtaOrchestrator {
199
201
  // We cannot see which form the host used — the identity check after
200
202
  // restart is what proves it — but naming the right fix up front turns
201
203
  // a confusing silent failure into a one-line correction.
202
- __DEV__ && _internalLogger.logger.debug('[OTA] React Native >= 0.82 detected. The only valid Android integration is ' + 'getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) — ' + 'ReactNativeHost and its getJSBundleFile() override no longer exist.');
204
+ _internalLogger.logger.debug('[OTA] React Native >= 0.82 detected. The only valid Android integration is ' + 'getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) — ' + 'ReactNativeHost and its getJSBundleFile() override no longer exist.');
203
205
  }
204
206
  this.initialized = true;
205
207
  this.enabled = true;
208
+
209
+ // Device integrity signal.
210
+ //
211
+ // Reported, not enforced. Root defeats any check made from inside the app,
212
+ // so this is not a boundary — it is context. It tells an operator that a
213
+ // device's OTA slot is writable by other processes, which is what makes the
214
+ // launch-time hash re-check in SlotManager meaningful rather than paranoid.
215
+ // A host app that wants to withhold updates on such devices can read
216
+ // `deviceIntegrityIndicators()` and decide for itself.
217
+ try {
218
+ const raw = _NativeScaleBunOta.default?.getDeviceIntegrity?.();
219
+ this.integrityIndicators = raw ? JSON.parse(raw) : [];
220
+ if (this.integrityIndicators.length > 0) {
221
+ _internalLogger.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.');
222
+ }
223
+ } catch {
224
+ this.integrityIndicators = [];
225
+ }
206
226
  this.bootGuardConfig = config ?? {};
207
- __DEV__ && _internalLogger.logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? 'unknown'}` + `${this.environment.bridgeless ? ', bridgeless' : ''}` + `${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? '?'}` : ''})`);
227
+ _internalLogger.logger.debug(`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? 'unknown'}` + `${this.environment.bridgeless ? ', bridgeless' : ''}` + `${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? '?'}` : ''})`);
208
228
 
209
229
  // ONE read of the slot state, shared by both boot-time consumers.
210
230
  //
@@ -235,6 +255,22 @@ class OtaOrchestrator {
235
255
 
236
256
  // Prove the bundle we installed is the bundle that loaded.
237
257
  this.verifyRunningBundleIdentity();
258
+
259
+ // Boot-guard health is a LOCAL fact: this process booted from the staged
260
+ // bundle and did not crash. The heartbeat used to start only on the
261
+ // `action === 'NONE'` branch of sync(), so confirming a good bundle needed
262
+ // a successful network round-trip that returned exactly that answer — a
263
+ // device offline, rate-limited, or still being offered an update could
264
+ // never clear its marker, and the guard reverted a bundle that was working.
265
+ //
266
+ // It runs HERE, last, and is handed the slot state that was already read.
267
+ // Starting it earlier made it call getSlotState() itself, and that read is
268
+ // consume-on-read for the revert record — it swallowed the revert before
269
+ // checkBootGuardRecovery() could name the bundle that failed, so a
270
+ // crash-rollback went unreported. It also read bootGuardConfig before
271
+ // init() had assigned it, so healthyAfterMs silently fell back to the
272
+ // default.
273
+ this.startBootGuardHeartbeat(slotState);
238
274
  });
239
275
  }
240
276
 
@@ -273,7 +309,7 @@ class OtaOrchestrator {
273
309
  releaseId: record.releaseId ?? undefined,
274
310
  sha256: record.sha256
275
311
  };
276
- __DEV__ && _internalLogger.logger.debug(`[OTA] Running bundle v${record.version} (${record.bundleId})`);
312
+ _internalLogger.logger.debug(`[OTA] Running bundle v${record.version} (${record.bundleId})`);
277
313
  return;
278
314
  }
279
315
 
@@ -283,7 +319,7 @@ class OtaOrchestrator {
283
319
  this.currentBundle = {
284
320
  sha256: current.sha256
285
321
  };
286
- __DEV__ && _internalLogger.logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}…) with no local install record.`);
322
+ _internalLogger.logger.debug(`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}…) with no local install record.`);
287
323
  } catch {
288
324
  // Slot state unreadable — treat as factory bundle.
289
325
  }
@@ -331,7 +367,7 @@ class OtaOrchestrator {
331
367
  // this bundle" and answers by serving the same bundle again, forever.
332
368
  if (!record.identityToken) return;
333
369
  if (running === record.identityToken) {
334
- __DEV__ && _internalLogger.logger.debug('[OTA] Install verified — running bundle matches what was installed.');
370
+ _internalLogger.logger.debug('[OTA] Install verified — running bundle matches what was installed.');
335
371
  this.retireIdentityToken(record);
336
372
  return;
337
373
  }
@@ -464,6 +500,14 @@ class OtaOrchestrator {
464
500
  isEnabled() {
465
501
  return this.enabled;
466
502
  }
503
+
504
+ /**
505
+ * Root/jailbreak indicators detected on this device, as stable reason strings.
506
+ * Empty means nothing was detected — not proof the device is clean.
507
+ */
508
+ deviceIntegrityIndicators() {
509
+ return [...this.integrityIndicators];
510
+ }
467
511
  isRestartRequired() {
468
512
  return this.isRestartRequiredState;
469
513
  }
@@ -571,7 +615,7 @@ class OtaOrchestrator {
571
615
  error: 'OTA native module disabled or missing'
572
616
  };
573
617
  }
574
- __DEV__ && _internalLogger.logger.debug('[OTA] Sync started…');
618
+ _internalLogger.logger.debug('[OTA] Sync started…');
575
619
 
576
620
  // The funnel's denominator, for devices already on an OTA bundle. Emitted
577
621
  // before the request, so a check that fails outright still counts as a
@@ -590,7 +634,7 @@ class OtaOrchestrator {
590
634
  }
591
635
  const checkRes = await this.checkForUpdate(params);
592
636
  if (checkRes.action === 'NONE') {
593
- __DEV__ && _internalLogger.logger.debug('[OTA] App is up to date');
637
+ _internalLogger.logger.debug('[OTA] App is up to date');
594
638
  // If we booted from an OTA bundle and it's healthy, clear the boot guard
595
639
  this.startBootGuardHeartbeat();
596
640
  return {
@@ -648,7 +692,7 @@ class OtaOrchestrator {
648
692
  version: bundle.version
649
693
  });
650
694
  const downloadStart = Date.now();
651
- __DEV__ && _internalLogger.logger.debug(`[OTA] Downloading update v${bundle.version}…`);
695
+ _internalLogger.logger.debug(`[OTA] Downloading update v${bundle.version}…`);
652
696
 
653
697
  // Live progress: native tick → DOWNLOAD_PROGRESS (feeds useOtaUpdate) →
654
698
  // throttled POST to the live tracker. `bundle.size` is the authoritative
@@ -746,13 +790,13 @@ class OtaOrchestrator {
746
790
 
747
791
  // Sprint 7: Try patch first if offered, fall back to full bundle
748
792
  if (bundle.patch && _NativeScaleBunOta.default.stagePatch) {
749
- __DEV__ && _internalLogger.logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ` + `${(bundle.size / 1024).toFixed(0)} KB full) — trying patch…`);
793
+ _internalLogger.logger.debug(`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ` + `${(bundle.size / 1024).toFixed(0)} KB full) — trying patch…`);
750
794
  try {
751
795
  staged = await _NativeScaleBunOta.default.stagePatch(bundle.patch.url, bundle.patch.sha256, bundle.sha256 // expected full-bundle hash after patching
752
796
  );
753
797
  if (staged) {
754
798
  patchUsed = true;
755
- __DEV__ && _internalLogger.logger.debug('[OTA] Patch applied successfully');
799
+ _internalLogger.logger.debug('[OTA] Patch applied successfully');
756
800
  } else {
757
801
  _internalLogger.logger.warn('[OTA] Patch staging returned false — falling back to full bundle');
758
802
  }
@@ -843,7 +887,7 @@ class OtaOrchestrator {
843
887
  releaseId: bundle.releaseId,
844
888
  version: bundle.version
845
889
  });
846
- __DEV__ && _internalLogger.logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
890
+ _internalLogger.logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
847
891
 
848
892
  // Deliver telemetry BEFORE a restart tears the JS runtime down —
849
893
  // an INSTALLED event queued but not sent would vanish with the process.
@@ -889,7 +933,7 @@ class OtaOrchestrator {
889
933
  * native boot-guard marker. If JS crashes before the timer fires, the
890
934
  * marker stays set and native code reverts on next cold start.
891
935
  */
892
- startBootGuardHeartbeat() {
936
+ startBootGuardHeartbeat(preRead) {
893
937
  if (!_NativeScaleBunOta.default) return;
894
938
 
895
939
  // Cancel any existing timer
@@ -898,20 +942,23 @@ class OtaOrchestrator {
898
942
  this.healthyTimer = null;
899
943
  }
900
944
  try {
901
- const stateJson = _NativeScaleBunOta.default.getSlotState();
902
- const state = JSON.parse(stateJson);
945
+ // getSlotState() is CONSUME-ON-READ for the revert record, so a caller that
946
+ // has already read it passes the parsed result down rather than letting
947
+ // this read again and swallow a rollback. `null` is a real answer (module
948
+ // absent or payload unreadable) — only `undefined` means "nobody read it".
949
+ const state = preRead !== undefined ? preRead : JSON.parse(_NativeScaleBunOta.default.getSlotState());
903
950
 
904
951
  // Only run the heartbeat if boot-guard marker is present
905
952
  // (meaning we're running from a freshly-applied OTA bundle)
906
- if (!state.bootMarkerPresent) {
953
+ if (!state?.bootMarkerPresent) {
907
954
  return;
908
955
  }
909
956
  const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
910
- __DEV__ && _internalLogger.logger.debug(`[OTA] Boot guard active — marking healthy after ${healthyMs}ms`);
957
+ _internalLogger.logger.debug(`[OTA] Boot guard active — marking healthy after ${healthyMs}ms`);
911
958
  this.healthyTimer = setTimeout(() => {
912
959
  (0, _crashSafe.noThrow)(() => {
913
960
  _NativeScaleBunOta.default?.markHealthy();
914
- __DEV__ && _internalLogger.logger.info('[OTA] Boot guard cleared — bundle marked healthy ✓');
961
+ _internalLogger.logger.info('[OTA] Boot guard cleared — bundle marked healthy ✓');
915
962
  this.healthyTimer = null;
916
963
 
917
964
  // The honest activation signal. INSTALLED is emitted optimistically,
@@ -966,11 +1013,11 @@ class OtaOrchestrator {
966
1013
  /* the restart below makes cleanup moot */
967
1014
  }
968
1015
  this.resumeRestartArmed = false;
969
- __DEV__ && _internalLogger.logger.info('[OTA] App resumed — activating the staged update now');
1016
+ _internalLogger.logger.info('[OTA] App resumed — activating the staged update now');
970
1017
  this.restart();
971
1018
  }
972
1019
  });
973
- __DEV__ && _internalLogger.logger.debug('[OTA] Update staged — will activate on next resume (ON_NEXT_RESUME)');
1020
+ _internalLogger.logger.debug('[OTA] Update staged — will activate on next resume (ON_NEXT_RESUME)');
974
1021
  } catch {
975
1022
  // AppState unavailable (tests, exotic hosts): the update still activates
976
1023
  // on the next cold start, which is the documented fallback behaviour.
@@ -990,7 +1037,7 @@ class OtaOrchestrator {
990
1037
  */
991
1038
  restart() {
992
1039
  (0, _crashSafe.noThrow)(() => {
993
- __DEV__ && _internalLogger.logger.info('[OTA] Triggering app restart…');
1040
+ _internalLogger.logger.info('[OTA] Triggering app restart…');
994
1041
  if (_NativeScaleBunOta.default && typeof _NativeScaleBunOta.default.restartApp === 'function') {
995
1042
  _NativeScaleBunOta.default.restartApp();
996
1043
  } else if (_reactNative.DevSettings && typeof _reactNative.DevSettings.reload === 'function') {
@@ -959,6 +959,69 @@ class SessionManager {
959
959
  state_status: stateStatus,
960
960
  ui: details?.ui,
961
961
  target_id: details?.targetId,
962
+ /**
963
+ * RN-6 — shipped with the row so the join is auditable instead of assumed.
964
+ *
965
+ * Without it, an interaction with no `target_id` is ambiguous in the worst way:
966
+ * it could mean the control genuinely had no identity, or that the correlation
967
+ * refused and the identity exists but could not be attributed. Those are
968
+ * different defects with different owners, and the accounting lane cannot give
969
+ * an honest unplaceable REASON without knowing which.
970
+ *
971
+ * Undefined on the JS-fallback path (no native event, so nothing was joined),
972
+ * which is itself the correct answer there rather than a fabricated 'exact'.
973
+ */
974
+ correlation: details?.correlation,
975
+ correlation_delta_ms: details?.correlationDeltaMs,
976
+ correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
977
+ /**
978
+ * RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
979
+ * identifier from a structural inference. 'path' means the app authored no testID
980
+ * and the identity was derived from component names, which is useful but will
981
+ * change under refactoring; a dashboard that presents the two identically invites
982
+ * someone to trust a key that is about to move.
983
+ */
984
+ target_source: details?.targetSource,
985
+ /**
986
+ * RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
987
+ * 'none' means the app instrumented nothing the SDK could see; 'manual' means a
988
+ * <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
989
+ * different owners behind one number that today has none.
990
+ */
991
+ screen_source: details?.screenSource,
992
+ /**
993
+ * RN-13 — what makes "the heat is on this control" checkable instead of assumed.
994
+ * Absent when the control could not be measured synchronously, which is a fact worth
995
+ * storing: an unmeasurable control and a mis-placed tap are different findings.
996
+ */
997
+ target_rect: details?.targetRect,
998
+ /**
999
+ * RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
1000
+ * table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
1001
+ * these need no migration. Mobile simply never sent them.
1002
+ *
1003
+ * `scroll_source` says whether an offset was available at all: 'none' means the app
1004
+ * uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
1005
+ * the integration and not a tap at the top of a list.
1006
+ */
1007
+ scrollX: details?.scrollX,
1008
+ scrollY: details?.scrollY,
1009
+ scroll_source: details?.scrollSource,
1010
+ /**
1011
+ * RN-4 — what stops a recycled native view merging two different rows into one
1012
+ * hotspot. The key is the app's own keyExtractor output, so it is stable across
1013
+ * scrolling and across sessions; the index is carried so a recycled slot (same index,
1014
+ * different key) can be proven rather than inferred.
1015
+ */
1016
+ item_key: details?.itemKey,
1017
+ item_index: details?.itemIndex,
1018
+ /**
1019
+ * RN-15 — where in the control, not just which control. Turns "they tapped Add to
1020
+ * Cart" into "they tapped its left edge", which is what tells you whether a hit area
1021
+ * is right. Outside 0..1 is a hitSlop tap and is kept as such.
1022
+ */
1023
+ local_u: details?.localU,
1024
+ local_v: details?.localV,
962
1025
  gestureType,
963
1026
  x: details?.x,
964
1027
  y: details?.y,
@@ -175,7 +175,7 @@ class ScaleBunFacade {
175
175
  const realQueue = _container.coreContainer.get('PersistentQueue');
176
176
  const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
177
177
  if (buffered.length > 0) {
178
- __DEV__ && _internalLogger.logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
178
+ _internalLogger.logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
179
179
  for (const envelope of buffered) {
180
180
  realQueue.add(envelope);
181
181
  }
@@ -303,7 +303,9 @@ class ScaleBunFacade {
303
303
  }
304
304
  const clientKey = this._clientKey;
305
305
  const apiUrl = this._apiBaseUrl;
306
- if (!clientKey || !apiUrl) return;
306
+ if (!clientKey || !apiUrl) {
307
+ return;
308
+ }
307
309
  this._otaCheckInFlight = true;
308
310
  try {
309
311
  const info = await _bridgeAdapter.bridgeAdapter.getDeviceInfo();
@@ -360,7 +362,7 @@ class ScaleBunFacade {
360
362
  if (!dbgConfig) return;
361
363
  dbgConfig.__skipDeprecationWarning = true;
362
364
  this.enableDebug(dbgConfig);
363
- __DEV__ && _internalLogger.logger.info('[ScaleBun] Debug auto-connected via init({ debug }).');
365
+ _internalLogger.logger.info('[ScaleBun] Debug auto-connected via init({ debug }).');
364
366
  } catch (err) {
365
367
  _internalLogger.logger.warn('[ScaleBun] Auto debug connection failed:', err?.message);
366
368
  }
@@ -377,7 +379,7 @@ class ScaleBunFacade {
377
379
  const apiBaseUrl = this._apiBaseUrl;
378
380
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
379
381
  if (enabled && !appId) {
380
- __DEV__ && _internalLogger.logger.info('[ScaleBun] Envelope event tracking disabled — no appId in init config.');
382
+ _internalLogger.logger.info('[ScaleBun] Envelope event tracking disabled — no appId in init config.');
381
383
  }
382
384
  return;
383
385
  }
@@ -429,7 +431,7 @@ class ScaleBunFacade {
429
431
  onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue)
430
432
  });
431
433
  this._eventTracker.start();
432
- __DEV__ && _internalLogger.logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
434
+ _internalLogger.logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
433
435
 
434
436
  // Complete the device context from the native bridge.
435
437
  //
@@ -1111,7 +1113,7 @@ class ScaleBunFacade {
1111
1113
  const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
1112
1114
  const messages = cfg?.inAppMessages ?? [];
1113
1115
  const previews = messages.filter(m => m.preview === true || m.isTest === true);
1114
- __DEV__ && _internalLogger.logger.info(`[Engage] debugFetchInAppMessages → ${messages.length} message(s), ${previews.length} test preview(s)`);
1116
+ _internalLogger.logger.info(`[Engage] debugFetchInAppMessages → ${messages.length} message(s), ${previews.length} test preview(s)`);
1115
1117
  return messages;
1116
1118
  },
1117
1119
  /** Upload a survey/NPS/feedback response (idempotent, rides the outbox). */
@@ -1396,7 +1398,7 @@ class ScaleBunFacade {
1396
1398
  _internalLogger.logger.warn('SDK not initialized. Event dropped:', name);
1397
1399
  return;
1398
1400
  }
1399
- __DEV__ && _internalLogger.logger.debug('Tracking event:', name);
1401
+ _internalLogger.logger.debug('Tracking event:', name);
1400
1402
 
1401
1403
  // Fan this event out to the Engage trigger engine. Same single funnel,
1402
1404
  // extra observer — lets behavioral triggers (custom_event, failed_action,
@@ -1569,7 +1571,7 @@ class ScaleBunFacade {
1569
1571
  await Promise.all([this._eventTracker?.flush() ?? Promise.resolve(), adapter?.flushAnalytics() ?? Promise.resolve()]);
1570
1572
  return;
1571
1573
  }
1572
- __DEV__ && _internalLogger.logger.debug('Flushing events from persistent queue...');
1574
+ _internalLogger.logger.debug('Flushing events from persistent queue...');
1573
1575
  const httpClient = _container.coreContainer.get('HttpClient');
1574
1576
  await (0, _flushQueue.flushQueue)(this._getQueue(), httpClient, (0, _deviceId.getDeviceId)());
1575
1577
  });
@@ -1592,9 +1594,9 @@ class ScaleBunFacade {
1592
1594
  const bootstrapPerfFeature = (0, _debug.getPerformanceFeature)();
1593
1595
  if (bootstrapPerfFeature) {
1594
1596
  this._performanceFeature = bootstrapPerfFeature;
1595
- __DEV__ && _internalLogger.logger.debug('Performance feature linked from bootstrap');
1597
+ _internalLogger.logger.debug('Performance feature linked from bootstrap');
1596
1598
  }
1597
- __DEV__ && _internalLogger.logger.info('Debug enabled');
1599
+ _internalLogger.logger.info('Debug enabled');
1598
1600
  });
1599
1601
  }
1600
1602
 
@@ -1746,14 +1748,14 @@ class ScaleBunFacade {
1746
1748
  return false;
1747
1749
  }
1748
1750
  sm.startSession(metadata);
1749
- __DEV__ && _internalLogger.logger.info('session.start() ✅ sessionId:', sm.currentSessionId);
1751
+ _internalLogger.logger.info('session.start() ✅ sessionId:', sm.currentSessionId);
1750
1752
  return true;
1751
1753
  },
1752
1754
  end: reason => {
1753
1755
  const sm = _SessionManager.SessionManager.getExistingInstance();
1754
1756
  if (!sm || !sm.isActive) return false;
1755
1757
  sm.endSession(reason);
1756
- __DEV__ && _internalLogger.logger.info('session.end() ✅');
1758
+ _internalLogger.logger.info('session.end() ✅');
1757
1759
  return true;
1758
1760
  },
1759
1761
  emitEvent: (type, data) => {
@@ -1811,7 +1813,7 @@ class ScaleBunFacade {
1811
1813
  ...this._privacyConfig,
1812
1814
  ...config
1813
1815
  };
1814
- __DEV__ && _internalLogger.logger.debug('[report] Privacy config updated');
1816
+ _internalLogger.logger.debug('[report] Privacy config updated');
1815
1817
  },
1816
1818
  /** Set branding for exported reports. */
1817
1819
  setBranding: config => {
@@ -1819,7 +1821,7 @@ class ScaleBunFacade {
1819
1821
  ...this._brandingConfig,
1820
1822
  ...config
1821
1823
  };
1822
- __DEV__ && _internalLogger.logger.debug('[report] Branding config updated');
1824
+ _internalLogger.logger.debug('[report] Branding config updated');
1823
1825
  },
1824
1826
  /** Get current privacy config. */
1825
1827
  getPrivacy: () => ({
@@ -1874,9 +1876,9 @@ class ScaleBunFacade {
1874
1876
  AutoScreenDetector
1875
1877
  } = require('../features/navigation/AutoScreenDetector');
1876
1878
  AutoScreenDetector.getInstance().setNavigationRef(ref);
1877
- __DEV__ && _internalLogger.logger.info('[ScaleBun] Navigation ref set → auto screen detection active');
1879
+ _internalLogger.logger.info('[ScaleBun] Navigation ref set → auto screen detection active');
1878
1880
  } catch (err) {
1879
- __DEV__ && _internalLogger.logger.debug('[ScaleBun] Navigation ref stashed (detector not ready):', err?.message);
1881
+ _internalLogger.logger.debug('[ScaleBun] Navigation ref stashed (detector not ready):', err?.message);
1880
1882
  }
1881
1883
  });
1882
1884
  }
@@ -75,6 +75,15 @@ export class SDKBootstrapper {
75
75
  // apiBaseUrl is LOCKED: whatever an integrator passed (or the schema default) is
76
76
  // discarded here so the SDK can never be repointed away from the ScaleBun cloud.
77
77
  // Change DEFAULT_API_BASE_URL at build time to target a different backend.
78
+ // Say so when we discard it. The value was accepted by the schema, silently
79
+ // overwritten here, and the SDK then talked to a different server than the
80
+ // integrator configured — with no log, no warning, and no error. Someone
81
+ // pointing the SDK at a staging or local backend would watch requests arrive
82
+ // at production and have nothing to go on.
83
+ const requestedApiBaseUrl = rawConfig?.apiBaseUrl;
84
+ if (typeof requestedApiBaseUrl === 'string' && requestedApiBaseUrl && requestedApiBaseUrl !== DEFAULT_API_BASE_URL) {
85
+ logger.warn(`[ScaleBun] Ignoring apiBaseUrl '${requestedApiBaseUrl}' — 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.`);
86
+ }
78
87
  this.config.apiBaseUrl = DEFAULT_API_BASE_URL;
79
88
  // Explicit logLevel wins; otherwise debug:true implies verbose 'debug'.
80
89
  if (this.config.logLevel) {
@@ -314,6 +323,27 @@ export class SDKBootstrapper {
314
323
  await this.featureRegistry.initializeAll(featureContext);
315
324
 
316
325
  // ── Session Replay Initialization (Core) ───────────────
326
+ /**
327
+ * RN-3 zero-config scroll instrumentation, opt-in.
328
+ *
329
+ * Placed before replay init because it patches a module export and must be in place before the
330
+ * host renders its first scroll view. Reports rather than assumes: the equivalent React
331
+ * Navigation hook once claimed success it had not verified and left a whole pipeline silently
332
+ * empty, so a failure here is said out loud with a reason an integrator can act on.
333
+ */
334
+ if (this.config.autoInstrumentScrollViews === true) {
335
+ try {
336
+ const {
337
+ autoInstrumentScrollViews
338
+ } = require('../features/journey/autoInstrumentScroll');
339
+ const outcome = autoInstrumentScrollViews();
340
+ if (!outcome.installed) {
341
+ logger.warn('[Bootstrap] autoInstrumentScrollViews was requested but could not be ' + 'installed: ' + outcome.reason + ' — taps will carry no content depth.');
342
+ } else {
343
+ __DEV__ && logger.debug('[Bootstrap] ScrollView auto-instrumentation installed');
344
+ }
345
+ } catch {/* no-throw */}
346
+ }
317
347
  if (features.replay !== false) {
318
348
  try {
319
349
  __DEV__ && logger.debug('[Bootstrap] Initializing unified SessionManager');
@@ -269,6 +269,22 @@ const SHAPE = {
269
269
  }
270
270
  },
271
271
  captureInteractionHeatmap: bool(true),
272
+ /**
273
+ * RN-3 zero-config: patch React Native's ScrollView export so EVERY scroll view reports its
274
+ * offset, not just ones wrapped in ScaleBunScrollView.
275
+ *
276
+ * DEFAULT FALSE, unlike captureInteractionHeatmap above, and the difference is deliberate.
277
+ * That flag changes what the SDK records about the host; this one changes what the HOST
278
+ * RENDERS — every scroll view in the app, including ones inside third-party libraries the app
279
+ * did not write, starts routing through an SDK component. The failure mode is not a wrong
280
+ * number in a dashboard, it is the host's UI behaving differently because an analytics SDK
281
+ * replaced a primitive. That is a decision an integrator makes deliberately, not one they
282
+ * discover afterwards.
283
+ *
284
+ * With it off, a plain <ScrollView> yields no content depth and the tap says so
285
+ * (scroll_source: 'none') rather than defaulting its offset to 0.
286
+ */
287
+ autoInstrumentScrollViews: bool(false),
272
288
  // Opt-in: intercept console.* and ship the lines to the Diagnose → Logs lane in SaaS
273
289
  // mode. OFF by default — console output can carry PII/secrets and adds ingest volume,
274
290
  // so capturing it is the host's explicit choice, not a silent default. When true and a
@@ -8,5 +8,5 @@
8
8
  * value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
9
9
  * version was introduced to solve.
10
10
  */
11
- export const SDK_VERSION = '2.0.0';
11
+ export const SDK_VERSION = '2.0.1';
12
12
  //# sourceMappingURL=version.js.map