@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.
- package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
- package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +18 -3
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +62 -1
- package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +2 -0
- package/dist/scalebun.full.js +840 -114
- package/dist/scalebun.slim.js +838 -112
- package/ios/ReplaySdk.swift +65 -2
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/commonjs/core/config/schema.js +16 -0
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
- package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
- package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
- package/lib/commonjs/features/journey/scrollContext.js +146 -0
- package/lib/commonjs/features/journey/targetGeometry.js +164 -0
- package/lib/commonjs/features/journey/touchTarget.js +152 -24
- package/lib/commonjs/features/journey/uiState.js +81 -1
- package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/commonjs/features/ota/OtaOrchestrator.js +69 -22
- package/lib/commonjs/features/session/SessionManager.js +63 -0
- package/lib/commonjs/public/ScaleBunFacade.js +18 -16
- package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/module/core/config/schema.js +16 -0
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
- package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
- package/lib/module/features/journey/interactionProtocol.js +128 -8
- package/lib/module/features/journey/scrollContext.js +135 -0
- package/lib/module/features/journey/targetGeometry.js +155 -0
- package/lib/module/features/journey/touchTarget.js +151 -24
- package/lib/module/features/journey/uiState.js +78 -1
- package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/module/features/ota/OtaOrchestrator.js +69 -22
- package/lib/module/features/session/SessionManager.js +63 -0
- package/lib/module/public/ScaleBunFacade.js +18 -16
- package/lib/typescript/core/config/schema.d.ts +2 -0
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
- package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
- package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
- package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
- package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
- package/lib/typescript/features/journey/uiState.d.ts +39 -0
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +7 -0
- package/lib/typescript/features/session/SessionManager.d.ts +55 -0
- package/lib/typescript/public/types.d.ts +10 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +2 -0
- package/package.json +2 -2
- package/src/bootstrap/SDKBootstrapper.ts +40 -0
- package/src/core/config/schema.ts +18 -0
- package/src/core/constants/version.ts +1 -1
- package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
- package/src/features/journey/ScaleBunScrollView.tsx +29 -0
- package/src/features/journey/autoInstrumentScroll.ts +155 -0
- package/src/features/journey/interactionProtocol.ts +188 -10
- package/src/features/journey/scrollContext.ts +137 -0
- package/src/features/journey/targetGeometry.ts +176 -0
- package/src/features/journey/touchTarget.ts +237 -26
- package/src/features/journey/uiState.ts +80 -1
- package/src/features/navigation/AutoScreenDetector.ts +65 -5
- package/src/features/ota/OtaOrchestrator.ts +76 -22
- package/src/features/session/SessionManager.ts +113 -0
- package/src/public/ScaleBunFacade.ts +18 -16
- package/src/public/types.ts +10 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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
|
|
953
|
+
if (!state?.bootMarkerPresent) {
|
|
907
954
|
return;
|
|
908
955
|
}
|
|
909
956
|
const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
+
_internalLogger.logger.info('[OTA] App resumed — activating the staged update now');
|
|
970
1017
|
this.restart();
|
|
971
1018
|
}
|
|
972
1019
|
});
|
|
973
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1597
|
+
_internalLogger.logger.debug('Performance feature linked from bootstrap');
|
|
1596
1598
|
}
|
|
1597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1879
|
+
_internalLogger.logger.info('[ScaleBun] Navigation ref set → auto screen detection active');
|
|
1878
1880
|
} catch (err) {
|
|
1879
|
-
|
|
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
|