@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
|
@@ -137,12 +137,12 @@ async function deliverOtaEvents(params: {
|
|
|
137
137
|
},
|
|
138
138
|
);
|
|
139
139
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
140
|
-
|
|
140
|
+
logger.debug(`[OTA] Delivered ${items.length} telemetry event(s)`);
|
|
141
141
|
} catch (err: any) {
|
|
142
142
|
// Failed delivery must not fail the update — but the events must not be
|
|
143
143
|
// lost either: requeue so the next sync retries them.
|
|
144
144
|
for (const e of events) otaEventEmitter.emit(e);
|
|
145
|
-
|
|
145
|
+
logger.debug(`[OTA] Telemetry delivery failed (will retry next sync): ${err?.message}`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -188,6 +188,8 @@ export class OtaOrchestrator {
|
|
|
188
188
|
return this.currentBundle;
|
|
189
189
|
}
|
|
190
190
|
private isRestartRequiredState = false;
|
|
191
|
+
/** Root/jailbreak indicators observed at init; empty when none found. */
|
|
192
|
+
private integrityIndicators: string[] = [];
|
|
191
193
|
private healthyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
192
194
|
private bootGuardConfig: BootGuardConfig = {};
|
|
193
195
|
private environment: OtaEnvironment | null = null;
|
|
@@ -247,7 +249,7 @@ export class OtaOrchestrator {
|
|
|
247
249
|
// We cannot see which form the host used — the identity check after
|
|
248
250
|
// restart is what proves it — but naming the right fix up front turns
|
|
249
251
|
// a confusing silent failure into a one-line correction.
|
|
250
|
-
|
|
252
|
+
logger.debug(
|
|
251
253
|
'[OTA] React Native >= 0.82 detected. The only valid Android integration is ' +
|
|
252
254
|
'getDefaultReactHost(..., jsBundleFilePath = ScaleBunOtaModule.getJSBundleFile(ctx)) — ' +
|
|
253
255
|
'ReactNativeHost and its getJSBundleFile() override no longer exist.',
|
|
@@ -256,8 +258,30 @@ export class OtaOrchestrator {
|
|
|
256
258
|
|
|
257
259
|
this.initialized = true;
|
|
258
260
|
this.enabled = true;
|
|
261
|
+
|
|
262
|
+
// Device integrity signal.
|
|
263
|
+
//
|
|
264
|
+
// Reported, not enforced. Root defeats any check made from inside the app,
|
|
265
|
+
// so this is not a boundary — it is context. It tells an operator that a
|
|
266
|
+
// device's OTA slot is writable by other processes, which is what makes the
|
|
267
|
+
// launch-time hash re-check in SlotManager meaningful rather than paranoid.
|
|
268
|
+
// A host app that wants to withhold updates on such devices can read
|
|
269
|
+
// `deviceIntegrityIndicators()` and decide for itself.
|
|
270
|
+
try {
|
|
271
|
+
const raw = NativeScaleBunOta?.getDeviceIntegrity?.();
|
|
272
|
+
this.integrityIndicators = raw ? (JSON.parse(raw) as string[]) : [];
|
|
273
|
+
if (this.integrityIndicators.length > 0) {
|
|
274
|
+
logger.warn(
|
|
275
|
+
`[OTA] Device integrity indicators present: ${this.integrityIndicators.join(', ')}. ` +
|
|
276
|
+
'Updates still install and are still signature-verified, but an installed ' +
|
|
277
|
+
'bundle on this device can be modified by other privileged processes.',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
this.integrityIndicators = [];
|
|
282
|
+
}
|
|
259
283
|
this.bootGuardConfig = config ?? {};
|
|
260
|
-
|
|
284
|
+
logger.debug(
|
|
261
285
|
`[OTA] Orchestrator initialized (RN ${this.environment.rnVersionString ?? 'unknown'}` +
|
|
262
286
|
`${this.environment.bridgeless ? ', bridgeless' : ''}` +
|
|
263
287
|
`${this.environment.hermes ? `, Hermes HBC v${this.environment.hermesBytecodeVersion ?? '?'}` : ''})`,
|
|
@@ -292,6 +316,22 @@ export class OtaOrchestrator {
|
|
|
292
316
|
|
|
293
317
|
// Prove the bundle we installed is the bundle that loaded.
|
|
294
318
|
this.verifyRunningBundleIdentity();
|
|
319
|
+
|
|
320
|
+
// Boot-guard health is a LOCAL fact: this process booted from the staged
|
|
321
|
+
// bundle and did not crash. The heartbeat used to start only on the
|
|
322
|
+
// `action === 'NONE'` branch of sync(), so confirming a good bundle needed
|
|
323
|
+
// a successful network round-trip that returned exactly that answer — a
|
|
324
|
+
// device offline, rate-limited, or still being offered an update could
|
|
325
|
+
// never clear its marker, and the guard reverted a bundle that was working.
|
|
326
|
+
//
|
|
327
|
+
// It runs HERE, last, and is handed the slot state that was already read.
|
|
328
|
+
// Starting it earlier made it call getSlotState() itself, and that read is
|
|
329
|
+
// consume-on-read for the revert record — it swallowed the revert before
|
|
330
|
+
// checkBootGuardRecovery() could name the bundle that failed, so a
|
|
331
|
+
// crash-rollback went unreported. It also read bootGuardConfig before
|
|
332
|
+
// init() had assigned it, so healthyAfterMs silently fell back to the
|
|
333
|
+
// default.
|
|
334
|
+
this.startBootGuardHeartbeat(slotState);
|
|
295
335
|
});
|
|
296
336
|
}
|
|
297
337
|
|
|
@@ -331,7 +371,7 @@ export class OtaOrchestrator {
|
|
|
331
371
|
releaseId: record.releaseId ?? undefined,
|
|
332
372
|
sha256: record.sha256,
|
|
333
373
|
} as OtaBundlePayload;
|
|
334
|
-
|
|
374
|
+
logger.debug(`[OTA] Running bundle v${record.version} (${record.bundleId})`);
|
|
335
375
|
return;
|
|
336
376
|
}
|
|
337
377
|
|
|
@@ -339,7 +379,7 @@ export class OtaOrchestrator {
|
|
|
339
379
|
// a reinstall of the app, cleared storage, or a bundle staged by an
|
|
340
380
|
// older SDK. Report the hash so the server can still recognise it.
|
|
341
381
|
this.currentBundle = { sha256: current.sha256 } as OtaBundlePayload;
|
|
342
|
-
|
|
382
|
+
logger.debug(
|
|
343
383
|
`[OTA] Running an OTA bundle (sha ${String(current.sha256).slice(0, 12)}…) with no local install record.`,
|
|
344
384
|
);
|
|
345
385
|
} catch {
|
|
@@ -391,7 +431,7 @@ export class OtaOrchestrator {
|
|
|
391
431
|
if (!record.identityToken) return;
|
|
392
432
|
|
|
393
433
|
if (running === record.identityToken) {
|
|
394
|
-
|
|
434
|
+
logger.debug('[OTA] Install verified — running bundle matches what was installed.');
|
|
395
435
|
this.retireIdentityToken(record);
|
|
396
436
|
return;
|
|
397
437
|
}
|
|
@@ -563,6 +603,14 @@ export class OtaOrchestrator {
|
|
|
563
603
|
return this.enabled;
|
|
564
604
|
}
|
|
565
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Root/jailbreak indicators detected on this device, as stable reason strings.
|
|
608
|
+
* Empty means nothing was detected — not proof the device is clean.
|
|
609
|
+
*/
|
|
610
|
+
deviceIntegrityIndicators(): string[] {
|
|
611
|
+
return [...this.integrityIndicators];
|
|
612
|
+
}
|
|
613
|
+
|
|
566
614
|
isRestartRequired(): boolean {
|
|
567
615
|
return this.isRestartRequiredState;
|
|
568
616
|
}
|
|
@@ -698,7 +746,7 @@ export class OtaOrchestrator {
|
|
|
698
746
|
return { status: 'ERROR', error: 'OTA native module disabled or missing' };
|
|
699
747
|
}
|
|
700
748
|
|
|
701
|
-
|
|
749
|
+
logger.debug('[OTA] Sync started…');
|
|
702
750
|
|
|
703
751
|
// The funnel's denominator, for devices already on an OTA bundle. Emitted
|
|
704
752
|
// before the request, so a check that fails outright still counts as a
|
|
@@ -719,7 +767,7 @@ export class OtaOrchestrator {
|
|
|
719
767
|
const checkRes = await this.checkForUpdate(params);
|
|
720
768
|
|
|
721
769
|
if (checkRes.action === 'NONE') {
|
|
722
|
-
|
|
770
|
+
logger.debug('[OTA] App is up to date');
|
|
723
771
|
// If we booted from an OTA bundle and it's healthy, clear the boot guard
|
|
724
772
|
this.startBootGuardHeartbeat();
|
|
725
773
|
return { status: 'UP_TO_DATE' };
|
|
@@ -770,7 +818,7 @@ export class OtaOrchestrator {
|
|
|
770
818
|
version: bundle.version,
|
|
771
819
|
});
|
|
772
820
|
const downloadStart = Date.now();
|
|
773
|
-
|
|
821
|
+
logger.debug(`[OTA] Downloading update v${bundle.version}…`);
|
|
774
822
|
|
|
775
823
|
// Live progress: native tick → DOWNLOAD_PROGRESS (feeds useOtaUpdate) →
|
|
776
824
|
// throttled POST to the live tracker. `bundle.size` is the authoritative
|
|
@@ -871,7 +919,7 @@ export class OtaOrchestrator {
|
|
|
871
919
|
|
|
872
920
|
// Sprint 7: Try patch first if offered, fall back to full bundle
|
|
873
921
|
if (bundle.patch && NativeScaleBunOta.stagePatch) {
|
|
874
|
-
|
|
922
|
+
logger.debug(
|
|
875
923
|
`[OTA] Patch available (${(bundle.patch.size / 1024).toFixed(0)} KB vs ` +
|
|
876
924
|
`${(bundle.size / 1024).toFixed(0)} KB full) — trying patch…`,
|
|
877
925
|
);
|
|
@@ -883,7 +931,7 @@ export class OtaOrchestrator {
|
|
|
883
931
|
);
|
|
884
932
|
if (staged) {
|
|
885
933
|
patchUsed = true;
|
|
886
|
-
|
|
934
|
+
logger.debug('[OTA] Patch applied successfully');
|
|
887
935
|
} else {
|
|
888
936
|
logger.warn('[OTA] Patch staging returned false — falling back to full bundle');
|
|
889
937
|
}
|
|
@@ -972,7 +1020,7 @@ export class OtaOrchestrator {
|
|
|
972
1020
|
releaseId: bundle.releaseId,
|
|
973
1021
|
version: bundle.version,
|
|
974
1022
|
});
|
|
975
|
-
|
|
1023
|
+
logger.debug(`[OTA] Update v${bundle.version} installed successfully!`);
|
|
976
1024
|
|
|
977
1025
|
// Deliver telemetry BEFORE a restart tears the JS runtime down —
|
|
978
1026
|
// an INSTALLED event queued but not sent would vanish with the process.
|
|
@@ -1016,7 +1064,7 @@ export class OtaOrchestrator {
|
|
|
1016
1064
|
* native boot-guard marker. If JS crashes before the timer fires, the
|
|
1017
1065
|
* marker stays set and native code reverts on next cold start.
|
|
1018
1066
|
*/
|
|
1019
|
-
private startBootGuardHeartbeat(): void {
|
|
1067
|
+
private startBootGuardHeartbeat(preRead?: Record<string, any> | null): void {
|
|
1020
1068
|
if (!NativeScaleBunOta) return;
|
|
1021
1069
|
|
|
1022
1070
|
// Cancel any existing timer
|
|
@@ -1026,22 +1074,28 @@ export class OtaOrchestrator {
|
|
|
1026
1074
|
}
|
|
1027
1075
|
|
|
1028
1076
|
try {
|
|
1029
|
-
|
|
1030
|
-
|
|
1077
|
+
// getSlotState() is CONSUME-ON-READ for the revert record, so a caller that
|
|
1078
|
+
// has already read it passes the parsed result down rather than letting
|
|
1079
|
+
// this read again and swallow a rollback. `null` is a real answer (module
|
|
1080
|
+
// absent or payload unreadable) — only `undefined` means "nobody read it".
|
|
1081
|
+
const state =
|
|
1082
|
+
preRead !== undefined
|
|
1083
|
+
? preRead
|
|
1084
|
+
: (JSON.parse(NativeScaleBunOta.getSlotState()) as Record<string, any>);
|
|
1031
1085
|
|
|
1032
1086
|
// Only run the heartbeat if boot-guard marker is present
|
|
1033
1087
|
// (meaning we're running from a freshly-applied OTA bundle)
|
|
1034
|
-
if (!state
|
|
1088
|
+
if (!state?.bootMarkerPresent) {
|
|
1035
1089
|
return;
|
|
1036
1090
|
}
|
|
1037
1091
|
|
|
1038
1092
|
const healthyMs = this.bootGuardConfig.healthyAfterMs ?? DEFAULT_HEALTHY_AFTER_MS;
|
|
1039
|
-
|
|
1093
|
+
logger.debug(`[OTA] Boot guard active — marking healthy after ${healthyMs}ms`);
|
|
1040
1094
|
|
|
1041
1095
|
this.healthyTimer = setTimeout(() => {
|
|
1042
1096
|
noThrow(() => {
|
|
1043
1097
|
NativeScaleBunOta?.markHealthy();
|
|
1044
|
-
|
|
1098
|
+
logger.info('[OTA] Boot guard cleared — bundle marked healthy ✓');
|
|
1045
1099
|
this.healthyTimer = null;
|
|
1046
1100
|
|
|
1047
1101
|
// The honest activation signal. INSTALLED is emitted optimistically,
|
|
@@ -1097,11 +1151,11 @@ export class OtaOrchestrator {
|
|
|
1097
1151
|
/* the restart below makes cleanup moot */
|
|
1098
1152
|
}
|
|
1099
1153
|
this.resumeRestartArmed = false;
|
|
1100
|
-
|
|
1154
|
+
logger.info('[OTA] App resumed — activating the staged update now');
|
|
1101
1155
|
this.restart();
|
|
1102
1156
|
}
|
|
1103
1157
|
});
|
|
1104
|
-
|
|
1158
|
+
logger.debug('[OTA] Update staged — will activate on next resume (ON_NEXT_RESUME)');
|
|
1105
1159
|
} catch {
|
|
1106
1160
|
// AppState unavailable (tests, exotic hosts): the update still activates
|
|
1107
1161
|
// on the next cold start, which is the documented fallback behaviour.
|
|
@@ -1121,7 +1175,7 @@ export class OtaOrchestrator {
|
|
|
1121
1175
|
*/
|
|
1122
1176
|
restart(): void {
|
|
1123
1177
|
noThrow(() => {
|
|
1124
|
-
|
|
1178
|
+
logger.info('[OTA] Triggering app restart…');
|
|
1125
1179
|
if (NativeScaleBunOta && typeof NativeScaleBunOta.restartApp === 'function') {
|
|
1126
1180
|
NativeScaleBunOta.restartApp();
|
|
1127
1181
|
} else if (DevSettings && typeof DevSettings.reload === 'function') {
|
|
@@ -1105,6 +1105,56 @@ export class SessionManager {
|
|
|
1105
1105
|
targetId?: string;
|
|
1106
1106
|
screenName?: string;
|
|
1107
1107
|
emitAutomaticAnalytics?: boolean;
|
|
1108
|
+
/**
|
|
1109
|
+
* RN-6 — the native↔JS join's own verdict, and it MUST be declared here.
|
|
1110
|
+
*
|
|
1111
|
+
* This signature ENUMERATES its fields rather than spreading, and the only caller
|
|
1112
|
+
* (ScaleBunDebugRoot's native listener) invokes it through `(sm as any)`. So a field
|
|
1113
|
+
* added at the call site and not added here is dropped in silence, with no type
|
|
1114
|
+
* error anywhere — which is precisely how these three nearly shipped as a no-op.
|
|
1115
|
+
*
|
|
1116
|
+
* 'exact' = one JS start was clearly closest. 'ambiguous' = two were within
|
|
1117
|
+
* CORRELATION_MARGIN_MS of each other, so target/ui/screenName were deliberately
|
|
1118
|
+
* withheld rather than guessed. 'none' = no start within tolerance, meaning the JS
|
|
1119
|
+
* handler never fired. The last two are kept distinct because they have different
|
|
1120
|
+
* fixes and collapsing them would hide which is happening in production.
|
|
1121
|
+
*/
|
|
1122
|
+
correlation?: 'exact' | 'ambiguous' | 'none';
|
|
1123
|
+
correlationDeltaMs?: number | null;
|
|
1124
|
+
correlationRunnerUpDeltaMs?: number | null;
|
|
1125
|
+
/**
|
|
1126
|
+
* RN-1/RN-2 — which kind of identity `targetId` is. Declared here for the same reason as
|
|
1127
|
+
* the correlation fields above: this signature enumerates, and its only caller goes
|
|
1128
|
+
* through `(sm as any)`, so an undeclared field is dropped in silence.
|
|
1129
|
+
*/
|
|
1130
|
+
targetSource?: 'testID' | 'path' | 'component' | 'none';
|
|
1131
|
+
/** RN-8: where the screen name came from. Declared here because this signature enumerates. */
|
|
1132
|
+
screenSource?: 'manual' | 'navigation' | 'none';
|
|
1133
|
+
/**
|
|
1134
|
+
* RN-13: the target control's rectangle in capture-base fractions. Declared here because
|
|
1135
|
+
* this signature ENUMERATES — the sixth field to need it, after three correlation fields,
|
|
1136
|
+
* targetSource and screenSource.
|
|
1137
|
+
*/
|
|
1138
|
+
targetRect?: { x: number; y: number; width: number; height: number };
|
|
1139
|
+
/**
|
|
1140
|
+
* RN-3: content offset of the nearest ancestor scroller. Declared here because this
|
|
1141
|
+
* signature ENUMERATES — the seventh field to need it.
|
|
1142
|
+
*/
|
|
1143
|
+
scrollX?: number;
|
|
1144
|
+
scrollY?: number;
|
|
1145
|
+
scrollSource?: 'wrapper' | 'none';
|
|
1146
|
+
/**
|
|
1147
|
+
* RN-4: list row identity. Declared here because this signature ENUMERATES — the eighth
|
|
1148
|
+
* and ninth fields to need it.
|
|
1149
|
+
*/
|
|
1150
|
+
itemKey?: string;
|
|
1151
|
+
itemIndex?: number;
|
|
1152
|
+
/**
|
|
1153
|
+
* RN-15: tap position within the target control. Declared here because this signature
|
|
1154
|
+
* ENUMERATES — the tenth and eleventh fields to need it.
|
|
1155
|
+
*/
|
|
1156
|
+
localU?: number;
|
|
1157
|
+
localV?: number;
|
|
1108
1158
|
}): void {
|
|
1109
1159
|
// Exact-duplicate suppression — see _lastGestureSig. Signature is the gesture type plus
|
|
1110
1160
|
// raw coordinates verbatim; String() keeps undefined coords distinct from 0 (a payload
|
|
@@ -1136,6 +1186,69 @@ export class SessionManager {
|
|
|
1136
1186
|
state_status: stateStatus,
|
|
1137
1187
|
ui: details?.ui,
|
|
1138
1188
|
target_id: details?.targetId,
|
|
1189
|
+
/**
|
|
1190
|
+
* RN-6 — shipped with the row so the join is auditable instead of assumed.
|
|
1191
|
+
*
|
|
1192
|
+
* Without it, an interaction with no `target_id` is ambiguous in the worst way:
|
|
1193
|
+
* it could mean the control genuinely had no identity, or that the correlation
|
|
1194
|
+
* refused and the identity exists but could not be attributed. Those are
|
|
1195
|
+
* different defects with different owners, and the accounting lane cannot give
|
|
1196
|
+
* an honest unplaceable REASON without knowing which.
|
|
1197
|
+
*
|
|
1198
|
+
* Undefined on the JS-fallback path (no native event, so nothing was joined),
|
|
1199
|
+
* which is itself the correct answer there rather than a fabricated 'exact'.
|
|
1200
|
+
*/
|
|
1201
|
+
correlation: details?.correlation,
|
|
1202
|
+
correlation_delta_ms: details?.correlationDeltaMs,
|
|
1203
|
+
correlation_runner_up_delta_ms: details?.correlationRunnerUpDeltaMs,
|
|
1204
|
+
/**
|
|
1205
|
+
* RN-1/RN-2 — stored alongside `target_id` so a reader can tell an authored
|
|
1206
|
+
* identifier from a structural inference. 'path' means the app authored no testID
|
|
1207
|
+
* and the identity was derived from component names, which is useful but will
|
|
1208
|
+
* change under refactoring; a dashboard that presents the two identically invites
|
|
1209
|
+
* someone to trust a key that is about to move.
|
|
1210
|
+
*/
|
|
1211
|
+
target_source: details?.targetSource,
|
|
1212
|
+
/**
|
|
1213
|
+
* RN-8 — stored so "13.8% of interactions have a screen" becomes answerable.
|
|
1214
|
+
* 'none' means the app instrumented nothing the SDK could see; 'manual' means a
|
|
1215
|
+
* <ScaleBunScreen> beacon; 'navigation' means the React Navigation hook. Three
|
|
1216
|
+
* different owners behind one number that today has none.
|
|
1217
|
+
*/
|
|
1218
|
+
screen_source: details?.screenSource,
|
|
1219
|
+
/**
|
|
1220
|
+
* RN-13 — what makes "the heat is on this control" checkable instead of assumed.
|
|
1221
|
+
* Absent when the control could not be measured synchronously, which is a fact worth
|
|
1222
|
+
* storing: an unmeasurable control and a mis-placed tap are different findings.
|
|
1223
|
+
*/
|
|
1224
|
+
target_rect: details?.targetRect,
|
|
1225
|
+
/**
|
|
1226
|
+
* RN-3 — the backend already reads scrollX/scrollY off the payload into the fact
|
|
1227
|
+
* table's own columns (it has done since the web SDK, which sends `sx`/`sy`), so
|
|
1228
|
+
* these need no migration. Mobile simply never sent them.
|
|
1229
|
+
*
|
|
1230
|
+
* `scroll_source` says whether an offset was available at all: 'none' means the app
|
|
1231
|
+
* uses a plain <ScrollView> rather than the SDK's wrapper, which is a coverage gap in
|
|
1232
|
+
* the integration and not a tap at the top of a list.
|
|
1233
|
+
*/
|
|
1234
|
+
scrollX: details?.scrollX,
|
|
1235
|
+
scrollY: details?.scrollY,
|
|
1236
|
+
scroll_source: details?.scrollSource,
|
|
1237
|
+
/**
|
|
1238
|
+
* RN-4 — what stops a recycled native view merging two different rows into one
|
|
1239
|
+
* hotspot. The key is the app's own keyExtractor output, so it is stable across
|
|
1240
|
+
* scrolling and across sessions; the index is carried so a recycled slot (same index,
|
|
1241
|
+
* different key) can be proven rather than inferred.
|
|
1242
|
+
*/
|
|
1243
|
+
item_key: details?.itemKey,
|
|
1244
|
+
item_index: details?.itemIndex,
|
|
1245
|
+
/**
|
|
1246
|
+
* RN-15 — where in the control, not just which control. Turns "they tapped Add to
|
|
1247
|
+
* Cart" into "they tapped its left edge", which is what tells you whether a hit area
|
|
1248
|
+
* is right. Outside 0..1 is a hitSlop tap and is kept as such.
|
|
1249
|
+
*/
|
|
1250
|
+
local_u: details?.localU,
|
|
1251
|
+
local_v: details?.localV,
|
|
1139
1252
|
gestureType,
|
|
1140
1253
|
x: details?.x,
|
|
1141
1254
|
y: details?.y,
|
|
@@ -235,7 +235,7 @@ class ScaleBunFacade {
|
|
|
235
235
|
const realQueue = coreContainer.get<PersistentQueue>('PersistentQueue');
|
|
236
236
|
const buffered = this._preInitBuffer.popBatch(this._preInitBuffer.length());
|
|
237
237
|
if (buffered.length > 0) {
|
|
238
|
-
|
|
238
|
+
logger.debug(`[ScaleBun] Draining ${buffered.length} pre-init events into persistent queue`);
|
|
239
239
|
for (const envelope of buffered) {
|
|
240
240
|
realQueue.add(envelope);
|
|
241
241
|
}
|
|
@@ -369,7 +369,9 @@ class ScaleBunFacade {
|
|
|
369
369
|
}
|
|
370
370
|
const clientKey = this._clientKey;
|
|
371
371
|
const apiUrl = this._apiBaseUrl;
|
|
372
|
-
if (!clientKey || !apiUrl)
|
|
372
|
+
if (!clientKey || !apiUrl) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
373
375
|
|
|
374
376
|
this._otaCheckInFlight = true;
|
|
375
377
|
try {
|
|
@@ -430,7 +432,7 @@ class ScaleBunFacade {
|
|
|
430
432
|
if (!dbgConfig) return;
|
|
431
433
|
(dbgConfig as any).__skipDeprecationWarning = true;
|
|
432
434
|
this.enableDebug(dbgConfig);
|
|
433
|
-
|
|
435
|
+
logger.info('[ScaleBun] Debug auto-connected via init({ debug }).');
|
|
434
436
|
} catch (err: any) {
|
|
435
437
|
logger.warn('[ScaleBun] Auto debug connection failed:', err?.message);
|
|
436
438
|
}
|
|
@@ -447,7 +449,7 @@ class ScaleBunFacade {
|
|
|
447
449
|
const apiBaseUrl = this._apiBaseUrl;
|
|
448
450
|
if (!enabled || !appId || !clientKey || !apiBaseUrl) {
|
|
449
451
|
if (enabled && !appId) {
|
|
450
|
-
|
|
452
|
+
logger.info('[ScaleBun] Envelope event tracking disabled — no appId in init config.');
|
|
451
453
|
}
|
|
452
454
|
return;
|
|
453
455
|
}
|
|
@@ -499,7 +501,7 @@ class ScaleBunFacade {
|
|
|
499
501
|
onEvent: (eventName, revenue) => this._skanManager?.onEvent(eventName, revenue),
|
|
500
502
|
});
|
|
501
503
|
this._eventTracker.start();
|
|
502
|
-
|
|
504
|
+
logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
|
|
503
505
|
|
|
504
506
|
// Complete the device context from the native bridge.
|
|
505
507
|
//
|
|
@@ -1174,7 +1176,7 @@ class ScaleBunFacade {
|
|
|
1174
1176
|
const cfg = await (this._getEngageTransport()?.fetchEngageConfig(context) ?? Promise.resolve(null));
|
|
1175
1177
|
const messages = cfg?.inAppMessages ?? [];
|
|
1176
1178
|
const previews = messages.filter((m) => m.preview === true || m.isTest === true);
|
|
1177
|
-
|
|
1179
|
+
logger.info(
|
|
1178
1180
|
`[Engage] debugFetchInAppMessages → ${messages.length} message(s), ${previews.length} test preview(s)`,
|
|
1179
1181
|
);
|
|
1180
1182
|
return messages;
|
|
@@ -1485,7 +1487,7 @@ class ScaleBunFacade {
|
|
|
1485
1487
|
logger.warn('SDK not initialized. Event dropped:', name);
|
|
1486
1488
|
return;
|
|
1487
1489
|
}
|
|
1488
|
-
|
|
1490
|
+
logger.debug('Tracking event:', name);
|
|
1489
1491
|
|
|
1490
1492
|
// Fan this event out to the Engage trigger engine. Same single funnel,
|
|
1491
1493
|
// extra observer — lets behavioral triggers (custom_event, failed_action,
|
|
@@ -1650,7 +1652,7 @@ class ScaleBunFacade {
|
|
|
1650
1652
|
]);
|
|
1651
1653
|
return;
|
|
1652
1654
|
}
|
|
1653
|
-
|
|
1655
|
+
logger.debug('Flushing events from persistent queue...');
|
|
1654
1656
|
const httpClient = coreContainer.get<HttpClient>('HttpClient');
|
|
1655
1657
|
await flushQueue(this._getQueue(), httpClient, getDeviceId());
|
|
1656
1658
|
});
|
|
@@ -1678,10 +1680,10 @@ class ScaleBunFacade {
|
|
|
1678
1680
|
const bootstrapPerfFeature = _getPerformanceFeature();
|
|
1679
1681
|
if (bootstrapPerfFeature) {
|
|
1680
1682
|
this._performanceFeature = bootstrapPerfFeature;
|
|
1681
|
-
|
|
1683
|
+
logger.debug('Performance feature linked from bootstrap');
|
|
1682
1684
|
}
|
|
1683
1685
|
|
|
1684
|
-
|
|
1686
|
+
logger.info('Debug enabled');
|
|
1685
1687
|
});
|
|
1686
1688
|
}
|
|
1687
1689
|
|
|
@@ -1836,14 +1838,14 @@ class ScaleBunFacade {
|
|
|
1836
1838
|
return false;
|
|
1837
1839
|
}
|
|
1838
1840
|
sm.startSession(metadata as Parameters<SessionManager['startSession']>[0]);
|
|
1839
|
-
|
|
1841
|
+
logger.info('session.start() ✅ sessionId:', sm.currentSessionId);
|
|
1840
1842
|
return true;
|
|
1841
1843
|
},
|
|
1842
1844
|
end: (reason?: 'ended' | 'timeout'): boolean => {
|
|
1843
1845
|
const sm = SessionManager.getExistingInstance();
|
|
1844
1846
|
if (!sm || !sm.isActive) return false;
|
|
1845
1847
|
sm.endSession(reason);
|
|
1846
|
-
|
|
1848
|
+
logger.info('session.end() ✅');
|
|
1847
1849
|
return true;
|
|
1848
1850
|
},
|
|
1849
1851
|
emitEvent: (type: string, data?: Record<string, unknown>) => {
|
|
@@ -1899,13 +1901,13 @@ class ScaleBunFacade {
|
|
|
1899
1901
|
/** Set SDK-wide privacy settings for report generation. */
|
|
1900
1902
|
setPrivacy: (config: PrivacyConfig): void => {
|
|
1901
1903
|
this._privacyConfig = { ...this._privacyConfig, ...config };
|
|
1902
|
-
|
|
1904
|
+
logger.debug('[report] Privacy config updated');
|
|
1903
1905
|
},
|
|
1904
1906
|
|
|
1905
1907
|
/** Set branding for exported reports. */
|
|
1906
1908
|
setBranding: (config: BrandingConfig): void => {
|
|
1907
1909
|
this._brandingConfig = { ...this._brandingConfig, ...config };
|
|
1908
|
-
|
|
1910
|
+
logger.debug('[report] Branding config updated');
|
|
1909
1911
|
},
|
|
1910
1912
|
|
|
1911
1913
|
/** Get current privacy config. */
|
|
@@ -1956,9 +1958,9 @@ class ScaleBunFacade {
|
|
|
1956
1958
|
try {
|
|
1957
1959
|
const { AutoScreenDetector } = require('../features/navigation/AutoScreenDetector');
|
|
1958
1960
|
AutoScreenDetector.getInstance().setNavigationRef(ref);
|
|
1959
|
-
|
|
1961
|
+
logger.info('[ScaleBun] Navigation ref set → auto screen detection active');
|
|
1960
1962
|
} catch (err: any) {
|
|
1961
|
-
|
|
1963
|
+
logger.debug('[ScaleBun] Navigation ref stashed (detector not ready):', err?.message);
|
|
1962
1964
|
}
|
|
1963
1965
|
});
|
|
1964
1966
|
}
|
package/src/public/types.ts
CHANGED
|
@@ -334,6 +334,16 @@ export interface SimplifiedInitConfig {
|
|
|
334
334
|
appId?: string;
|
|
335
335
|
/** SDK client key (SaaS mode). Also used to authenticate the envelope lane. */
|
|
336
336
|
clientKey?: string;
|
|
337
|
+
/**
|
|
338
|
+
* Patch React Native's ScrollView export so every scroll view reports its scroll offset,
|
|
339
|
+
* giving taps their content depth without the app swapping to ScaleBunScrollView.
|
|
340
|
+
*
|
|
341
|
+
* OFF BY DEFAULT because it changes what the host renders, not merely what the SDK records:
|
|
342
|
+
* every scroll view in the app — including ones inside libraries you did not write — routes
|
|
343
|
+
* through an SDK wrapper. Turn it on deliberately. With it off, taps on a plain ScrollView
|
|
344
|
+
* report no content depth and say so, rather than reporting a fabricated offset of 0.
|
|
345
|
+
*/
|
|
346
|
+
autoInstrumentScrollViews?: boolean;
|
|
337
347
|
/** Platform OVERRIDE — auto-detected from Platform.OS when omitted (integrations should omit it). */
|
|
338
348
|
platform?: 'ios' | 'android' | 'web' | 'react_native';
|
|
339
349
|
/**
|
|
@@ -34,6 +34,9 @@ export interface Spec extends TurboModule {
|
|
|
34
34
|
*/
|
|
35
35
|
getTrustedKeyIds(): string;
|
|
36
36
|
|
|
37
|
+
/** JSON array of root/jailbreak indicator strings; [] when none found. */
|
|
38
|
+
getDeviceIntegrity(): string;
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* Returns JSON with current slot state:
|
|
39
42
|
* { current: { bundleId, version, sha256, installedAt } | null,
|