@scalebun/react-native 1.10.7 → 1.11.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/replaysdk/tracking/InteractionTracker.kt +25 -25
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
- package/dist/scalebun.full.js +467 -255
- package/dist/scalebun.slim.js +466 -254
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/ios/Ota/OtaSlotManager.swift +19 -5
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +8 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
- package/lib/commonjs/features/ota/OtaTypes.js +4 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/commonjs/public/ScaleBunFacade.js +115 -2
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +8 -1
- package/lib/module/features/ota/OtaOrchestrator.js +174 -48
- package/lib/module/features/ota/OtaTypes.js +1 -1
- package/lib/module/features/ota/useOtaUpdate.js +11 -2
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/module/public/ScaleBunFacade.js +115 -2
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
- package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
- package/lib/typescript/features/session/SessionManager.d.ts +15 -10
- package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
- package/package.json +4 -3
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +9 -4
- package/src/features/ota/OtaEventEmitter.ts +12 -0
- package/src/features/ota/OtaOrchestrator.ts +209 -62
- package/src/features/ota/OtaTypes.ts +37 -39
- package/src/features/ota/useOtaUpdate.ts +11 -2
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
- package/src/public/ScaleBunFacade.ts +127 -3
|
@@ -253,6 +253,11 @@ class ScaleBunFacade {
|
|
|
253
253
|
logger.warn('[ScaleBun] A signing key is pinned but no signature verifier is available. ' + 'Signature checking is fail-closed: updates will be REJECTED until one exists. ' + 'Install the optional peers `@noble/ed25519` + `@noble/hashes` (no further ' + 'code needed), or supply `ota.verifySignature`.');
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
// `healthyAfterMs` is the field BootGuardConfig actually declares.
|
|
257
|
+
// This passed `healthyTimeoutMs`, which nothing reads, so a host that
|
|
258
|
+
// tuned the boot-guard window silently got the 10s default instead.
|
|
259
|
+
// Both spellings are accepted so the older one keeps working.
|
|
260
|
+
const healthyAfterMs = typeof ota.healthyAfterMs === 'number' ? ota.healthyAfterMs : typeof ota.healthyTimeoutMs === 'number' ? ota.healthyTimeoutMs : undefined;
|
|
256
261
|
otaOrchestrator.init({
|
|
257
262
|
...(signingRequested ? {
|
|
258
263
|
signature: {
|
|
@@ -262,15 +267,123 @@ class ScaleBunFacade {
|
|
|
262
267
|
verifier
|
|
263
268
|
}
|
|
264
269
|
} : {}),
|
|
265
|
-
...(
|
|
266
|
-
|
|
270
|
+
...(healthyAfterMs !== undefined ? {
|
|
271
|
+
healthyAfterMs
|
|
267
272
|
} : {})
|
|
268
273
|
});
|
|
269
274
|
logger.info('[ScaleBun] OTA enabled from init config.');
|
|
275
|
+
|
|
276
|
+
// Run the checks the config asked for. Until this existed, `ota.enabled`
|
|
277
|
+
// initialised the orchestrator and then never checked anything: the
|
|
278
|
+
// documented `checkOnForeground` and `channelOverride` options were read
|
|
279
|
+
// by no code at all, and an app following the documented config received
|
|
280
|
+
// updates only if it ALSO drove `useOtaUpdate` or the CodePush shim by
|
|
281
|
+
// hand. Nothing logged, because "no update available" and "never asked"
|
|
282
|
+
// look identical from the outside.
|
|
283
|
+
this._startOtaChecks(ota);
|
|
270
284
|
} catch (err) {
|
|
271
285
|
logger.warn(`[ScaleBun] OTA init failed: ${err?.message ?? err}`);
|
|
272
286
|
}
|
|
273
287
|
}
|
|
288
|
+
|
|
289
|
+
/** Guards against overlapping config-driven OTA checks. */
|
|
290
|
+
_otaCheckInFlight = false;
|
|
291
|
+
/** Wall clock of the last config-driven check, for the foreground floor. */
|
|
292
|
+
_otaLastCheckAt = 0;
|
|
293
|
+
_otaForegroundListener = null;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Minimum gap between config-driven checks.
|
|
297
|
+
*
|
|
298
|
+
* A foreground transition is cheap to trigger — app switchers, permission
|
|
299
|
+
* dialogs and share sheets all produce one — so an unthrottled check would
|
|
300
|
+
* put a request on the hot path every time the user glanced away. Ten
|
|
301
|
+
* minutes is well below any realistic release cadence and well above that
|
|
302
|
+
* noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
|
|
303
|
+
* which is never throttled.
|
|
304
|
+
*/
|
|
305
|
+
static OTA_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Drive OTA checks from init config: once at startup, then on each
|
|
309
|
+
* foreground when `checkOnForeground` is on (the schema default).
|
|
310
|
+
*
|
|
311
|
+
* `appVersion` is resolved from the native bridge rather than asked of the
|
|
312
|
+
* integrator, because it gates the server's `targetAppVersion` semver check
|
|
313
|
+
* — sending a wrong or invented value is worse than sending none, and there
|
|
314
|
+
* is no honest default. If it cannot be resolved, the check is skipped with
|
|
315
|
+
* a warning instead of guessing.
|
|
316
|
+
*/
|
|
317
|
+
_startOtaChecks(ota) {
|
|
318
|
+
// Never in a debug build. `ScaleBunOtaModule.getJSBundleFile()` returns null
|
|
319
|
+
// there on purpose so Metro keeps ownership of the bundle — so a bundle
|
|
320
|
+
// downloaded in dev is installed into a slot that will never be loaded, and
|
|
321
|
+
// the identity check on the next launch then correctly observes that the
|
|
322
|
+
// running code is not what was installed and reports APPLY_FAILED. Checking
|
|
323
|
+
// at all in dev buys nothing and manufactures that false alarm. A developer
|
|
324
|
+
// testing the OTA path drives `useOtaUpdate().sync()` explicitly.
|
|
325
|
+
if (__DEV__) {
|
|
326
|
+
logger.info('[ScaleBun] OTA checks are skipped in debug builds (Metro owns the bundle).');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const runCheck = async trigger => {
|
|
330
|
+
if (this._otaCheckInFlight) return;
|
|
331
|
+
if (trigger === 'foreground' && Date.now() - this._otaLastCheckAt < ScaleBunFacade.OTA_MIN_CHECK_INTERVAL_MS) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const clientKey = this._clientKey;
|
|
335
|
+
const apiUrl = this._apiBaseUrl;
|
|
336
|
+
if (!clientKey || !apiUrl) return;
|
|
337
|
+
this._otaCheckInFlight = true;
|
|
338
|
+
try {
|
|
339
|
+
const info = await bridgeAdapter.getDeviceInfo();
|
|
340
|
+
const appVersion = info?.appVersion;
|
|
341
|
+
if (!appVersion) {
|
|
342
|
+
logger.warn('[ScaleBun] OTA check skipped — the app version could not be read from the ' + 'native bridge. Rebuild the native app, or drive checks yourself with ' + 'useOtaUpdate({ appVersion }).');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this._otaLastCheckAt = Date.now();
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
347
|
+
const {
|
|
348
|
+
otaOrchestrator
|
|
349
|
+
} = require('../features/ota/OtaOrchestrator');
|
|
350
|
+
await otaOrchestrator.sync({
|
|
351
|
+
apiUrl,
|
|
352
|
+
clientKey,
|
|
353
|
+
appVersion,
|
|
354
|
+
// The documented option, finally connected. Omitted means the
|
|
355
|
+
// server's `default` channel, exactly as before.
|
|
356
|
+
channelName: typeof ota.channelOverride === 'string' ? ota.channelOverride : undefined
|
|
357
|
+
// Never forced from config: the release's own installMode
|
|
358
|
+
// decides when the app restarts, and yanking the screen out
|
|
359
|
+
// from under a user is not a decision this switch should make.
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.warn(`[ScaleBun] OTA check failed: ${err?.message ?? err}`);
|
|
363
|
+
} finally {
|
|
364
|
+
this._otaCheckInFlight = false;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
void runCheck('startup');
|
|
368
|
+
if (ota.checkOnForeground === false) return;
|
|
369
|
+
if (this._otaForegroundListener) return; // idempotent across repeated init()
|
|
370
|
+
try {
|
|
371
|
+
// Through `appLifecycle`, not a second AppState subscription: the SDK
|
|
372
|
+
// already owns one and fanning out from it keeps every consumer on the
|
|
373
|
+
// same transition sequence.
|
|
374
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
375
|
+
const {
|
|
376
|
+
appLifecycle
|
|
377
|
+
} = require('../core/lifecycle/appLifecycle');
|
|
378
|
+
this._otaForegroundListener = state => {
|
|
379
|
+
if (state === 'active') void runCheck('foreground');
|
|
380
|
+
};
|
|
381
|
+
appLifecycle.addListener(this._otaForegroundListener);
|
|
382
|
+
} catch {
|
|
383
|
+
// Lifecycle unavailable (tests, exotic hosts) — the startup check stands.
|
|
384
|
+
this._otaForegroundListener = null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
274
387
|
_autoEnableDebug(debugConfig) {
|
|
275
388
|
try {
|
|
276
389
|
const dbgConfig = buildDebugConfigFromInitConfig(debugConfig);
|
|
@@ -125,7 +125,7 @@ export declare class EventTracker {
|
|
|
125
125
|
* Persisted so it survives the click→install→open gap. Call BEFORE start() ideally.
|
|
126
126
|
*/
|
|
127
127
|
setAttributionClickId(clickId: string): void;
|
|
128
|
-
track(eventName: string, properties?: Record<string, any
|
|
128
|
+
track(eventName: string, properties?: Record<string, any>, timestamp?: number): void;
|
|
129
129
|
trackPurchase(input: {
|
|
130
130
|
revenue: number;
|
|
131
131
|
currency: string;
|
|
@@ -18,9 +18,11 @@ export type AutomaticEventName = 'app_foregrounded' | 'app_backgrounded' | 'scre
|
|
|
18
18
|
export interface AutomaticEvent {
|
|
19
19
|
name: AutomaticEventName;
|
|
20
20
|
properties: Record<string, unknown>;
|
|
21
|
+
/** Original observation time. Delivery can be delayed while native coordinates resolve. */
|
|
22
|
+
timestamp?: number;
|
|
21
23
|
}
|
|
22
24
|
type AutomaticEventListener = (event: AutomaticEvent) => void;
|
|
23
|
-
export declare function emitAutomaticEvent(name: AutomaticEventName, properties?: Record<string, unknown
|
|
25
|
+
export declare function emitAutomaticEvent(name: AutomaticEventName, properties?: Record<string, unknown>, timestamp?: number): void;
|
|
24
26
|
export declare function subscribeAutomaticEvents(listener: AutomaticEventListener): () => void;
|
|
25
27
|
/** Test-only reset; intentionally not exported from the package entry point. */
|
|
26
28
|
export declare function resetAutomaticEventsForTests(): void;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ScaleBun SDK version. Sent with the session-start envelope so the dashboard
|
|
3
3
|
* can attribute telemetry to the SDK build that produced it.
|
|
4
|
-
* Keep in sync with package.json "version".
|
|
4
|
+
* Keep in sync with package.json "version" — `version.test.ts` fails when they drift.
|
|
5
|
+
*
|
|
6
|
+
* Why the test matters: the 1.11.0 bump missed this line, so the build would have reported itself as
|
|
7
|
+
* 1.10.6. Every "is the release live, and on what share of traffic" question is answered from this
|
|
8
|
+
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
9
|
+
* version was introduced to solve.
|
|
5
10
|
*/
|
|
6
|
-
export declare const SDK_VERSION = "1.
|
|
11
|
+
export declare const SDK_VERSION = "1.11.1";
|
|
7
12
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** One physical interaction, shared by replay and analytics projections. */
|
|
2
|
+
export declare const INTERACTION_PROTOCOL_VERSION = 1;
|
|
3
|
+
export type InteractionStateStatus = 'captured_empty' | 'captured_nonempty' | 'not_captured' | 'not_instrumented';
|
|
4
|
+
export interface InteractionStartContext {
|
|
5
|
+
interactionId: string;
|
|
6
|
+
occurredAt: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
target?: string;
|
|
10
|
+
targetId?: string;
|
|
11
|
+
screenName?: string;
|
|
12
|
+
ui?: string;
|
|
13
|
+
stateStatus: InteractionStateStatus;
|
|
14
|
+
emitAutomaticAnalytics: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function generateInteractionId(): string;
|
|
17
|
+
/** Match a native bridge event to the JS evidence sampled at the same finger-down. */
|
|
18
|
+
export declare function nearestInteractionStart<T extends InteractionStartContext>(starts: readonly T[], occurredAt: number, toleranceMs?: number): T | undefined;
|
|
19
|
+
/** Analytics is a projection of the same evidence; no second click is invented. */
|
|
20
|
+
export declare function automaticInteractionProperties(payload: Record<string, unknown>, screenName: string | undefined, canonicalMirror: boolean): Record<string, unknown>;
|
|
21
|
+
//# sourceMappingURL=interactionProtocol.d.ts.map
|
|
@@ -5,10 +5,24 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Sprint 5 deliverable (S5-NAT-2, S5-NAT-3).
|
|
7
7
|
*/
|
|
8
|
-
export type OtaEventType =
|
|
8
|
+
export type OtaEventType =
|
|
9
|
+
/** A check was made against the server. The denominator of the funnel. */
|
|
10
|
+
'CHECK'
|
|
11
|
+
/** The server answered with a bundle for this device — the offer, before any bytes move. */
|
|
12
|
+
| 'OFFERED' | 'DOWNLOAD_STARTED' | 'DOWNLOAD_PROGRESS' | 'DOWNLOAD_COMPLETE'
|
|
13
|
+
/** Staged and swapped. Emitted optimistically, BEFORE the bundle has booted. */
|
|
14
|
+
| 'INSTALLED'
|
|
15
|
+
/** The bundle booted and survived to the healthy mark — the honest activation signal. */
|
|
16
|
+
| 'BOOT_SUCCESS' | 'APPLY_FAILED' | 'AUTO_ROLLBACK' | 'MANUAL_ROLLBACK';
|
|
9
17
|
export interface OtaEvent {
|
|
10
18
|
type: OtaEventType;
|
|
11
19
|
bundleId: string;
|
|
20
|
+
/**
|
|
21
|
+
* The release this bundle was served as. Optional because a bundle installed
|
|
22
|
+
* by an older SDK has no recorded release; present on everything emitted by a
|
|
23
|
+
* current one, so the delivery funnel can key by release rather than bundle.
|
|
24
|
+
*/
|
|
25
|
+
releaseId?: string;
|
|
12
26
|
version?: number;
|
|
13
27
|
/** 0–100 for DOWNLOAD_PROGRESS */
|
|
14
28
|
progress?: number;
|
|
@@ -15,8 +15,13 @@ export interface BootGuardConfig {
|
|
|
15
15
|
*/
|
|
16
16
|
healthyAfterMs?: number;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* @deprecated Not honoured, and never was — nothing in JS reads this field.
|
|
19
|
+
*
|
|
20
|
+
* The boot-attempt limit lives in native code, where the counter it bounds is
|
|
21
|
+
* incremented (`MAX_BOOT_ATTEMPTS` in `SlotManager.kt` / `OtaSlotManager.swift`,
|
|
22
|
+
* both 2). Setting it here has no effect; the field is kept only so existing
|
|
23
|
+
* call sites keep compiling. Change the limit natively, or file a request for
|
|
24
|
+
* it to be plumbed through `initOutbox`-style native config.
|
|
20
25
|
*/
|
|
21
26
|
maxRevertAttempts?: number;
|
|
22
27
|
}
|
|
@@ -51,6 +56,11 @@ export declare class OtaOrchestrator {
|
|
|
51
56
|
* Read the active slot back into `currentBundle` so the next check reports
|
|
52
57
|
* what this device is genuinely running.
|
|
53
58
|
*/
|
|
59
|
+
/**
|
|
60
|
+
* Parse the native slot state once. Returns null when the module is absent or
|
|
61
|
+
* the payload is unreadable — every caller treats that as "factory bundle".
|
|
62
|
+
*/
|
|
63
|
+
private readSlotState;
|
|
54
64
|
private hydrateCurrentBundleFromSlots;
|
|
55
65
|
/**
|
|
56
66
|
* Compare the bundle the slot manager believes is active against the identity
|
|
@@ -71,7 +81,16 @@ export declare class OtaOrchestrator {
|
|
|
71
81
|
private static readonly INSTALL_EXPECTATION_KEY;
|
|
72
82
|
private recordInstallExpectation;
|
|
73
83
|
private readInstallRecord;
|
|
74
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Drop the identity token once the install has been proven, keeping the rest
|
|
86
|
+
* of the record.
|
|
87
|
+
*
|
|
88
|
+
* The record does two jobs: it proves an install took effect (once), and it
|
|
89
|
+
* maps the native slot's sha256 back to a bundle id (for the life of that
|
|
90
|
+
* bundle). Only the first job is finished after a successful verification, so
|
|
91
|
+
* only the token is retired.
|
|
92
|
+
*/
|
|
93
|
+
private retireIdentityToken;
|
|
75
94
|
private clearInstallExpectation;
|
|
76
95
|
private storage;
|
|
77
96
|
/**
|
|
@@ -7,42 +7,38 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
-
* OTA-TELEMETRY-SPEC —
|
|
10
|
+
* OTA-TELEMETRY-SPEC — status (spec §84-86)
|
|
11
11
|
* ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* types + this spec, not the runtime wiring.
|
|
12
|
+
* Items 1-3 are DONE. All were additive and backward-compatible: old installed
|
|
13
|
+
* clients simply omit the new fields and never emit the new types, and the
|
|
14
|
+
* backend ingests nullable columns. Because they alter device runtime behaviour,
|
|
15
|
+
* verify against a real RN build + device kill-test before release — Metro
|
|
16
|
+
* cannot exercise the boot guard.
|
|
18
17
|
*
|
|
19
|
-
* 1. releaseId end-to-end
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* - set `releaseId: bundle.releaseId` in the delivery mapper
|
|
24
|
-
* OtaOrchestrator.ts deliverOtaEvents (~:105) — currently only bundleId.
|
|
25
|
-
* Then ota_events carry releaseId and the funnel keys by release.
|
|
18
|
+
* 1. releaseId end-to-end — DONE. `recordInstallExpectation` persists the
|
|
19
|
+
* release id (so a rollback reported launches later can still name it),
|
|
20
|
+
* `hydrateCurrentBundleFromSlots` restores it, every emit site passes it, and
|
|
21
|
+
* `deliverOtaEvents` maps it onto the wire item.
|
|
26
22
|
*
|
|
27
|
-
* 2.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
23
|
+
* 2. CHECK + OFFERED — DONE. CHECK is emitted at the top of `sync()` for devices
|
|
24
|
+
* already on an OTA bundle (a device on the factory bundle has no owned
|
|
25
|
+
* bundle id to name, and the server drops rows it cannot resolve — those are
|
|
26
|
+
* counted server-side instead). OFFERED is emitted on `action === 'DOWNLOAD'`
|
|
27
|
+
* before any bytes move.
|
|
32
28
|
*
|
|
33
|
-
* 3.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* add 'VERIFIED' after signature check (~:635).
|
|
29
|
+
* 3. BOOT_SUCCESS — DONE. Emitted from the boot-guard heartbeat once the bundle
|
|
30
|
+
* has actually booted and survived to `healthyAfterMs`. INSTALLED is kept and
|
|
31
|
+
* still means "staged + swapped", but activation should be measured on
|
|
32
|
+
* BOOT_SUCCESS: INSTALLED fires before the bundle has run, so it credits
|
|
33
|
+
* bundles that installed and then crash-reverted.
|
|
39
34
|
*
|
|
40
|
-
* 4. Stamp the running OTA bundle onto session/crash telemetry
|
|
41
|
-
* SessionMetadata.bundleId is the NATIVE app package id, not the OTA bundle
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
35
|
+
* 4. Stamp the running OTA bundle onto session/crash telemetry — NOT DONE.
|
|
36
|
+
* `SessionMetadata.bundleId` is the NATIVE app package id, not the OTA bundle,
|
|
37
|
+
* so crashes cannot currently be attributed to the bundle that produced them
|
|
38
|
+
* (release-health crash impact). Needs optional otaBundleId?/otaBundleVersion?
|
|
39
|
+
* (distinct fields — do NOT overload bundleId) sourced from
|
|
40
|
+
* `otaOrchestrator.getCurrentBundle()` at session start, plus the matching
|
|
41
|
+
* backend columns and a Prisma migration.
|
|
46
42
|
*
|
|
47
43
|
* Do NOT repurpose errorCode (it already collapses failure-error vs rollback-
|
|
48
44
|
* reason); add a new optional field if the two must be distinguished.
|
|
@@ -126,7 +122,8 @@ export interface OtaPatchPayload {
|
|
|
126
122
|
sha256: string;
|
|
127
123
|
baseBundleId: string;
|
|
128
124
|
}
|
|
129
|
-
|
|
125
|
+
import type { OtaEventType as EmittedOtaEventType } from './OtaEventEmitter';
|
|
126
|
+
export type OtaEventType = Exclude<EmittedOtaEventType, 'DOWNLOAD_PROGRESS'>;
|
|
130
127
|
export interface OtaEventItem {
|
|
131
128
|
kind: 'ota_event';
|
|
132
129
|
bundleId: string;
|
|
@@ -50,6 +50,7 @@ export declare class JourneyEventPipeline {
|
|
|
50
50
|
journeyId?: string;
|
|
51
51
|
/** Frame that was on screen when this happened. Resolved by the caller; see sessionTypes. */
|
|
52
52
|
frameId?: string;
|
|
53
|
+
timestamp?: number;
|
|
53
54
|
}): JourneyEvent | null;
|
|
54
55
|
/**
|
|
55
56
|
* Anchor a crash event — protect nearby events from eviction.
|
|
@@ -17,6 +17,7 @@ import type { DebugTransport } from '../../debug/transport';
|
|
|
17
17
|
import type { SessionMetadata, SessionReplayConfig, JourneyEventType, JourneyEventSeverity, JourneyEventSource, CrashAnchor, NativeSyncMetadata, SyncDecisionCallback } from './sessionTypes';
|
|
18
18
|
import { type NativeCaptureFn } from './ReplayCaptureManager';
|
|
19
19
|
import type { BackendSessionAdapter } from './BackendSessionAdapter';
|
|
20
|
+
import { type InteractionStateStatus } from '../journey/interactionProtocol';
|
|
20
21
|
export interface SessionManagerConfig {
|
|
21
22
|
sessionReplay?: Partial<SessionReplayConfig>;
|
|
22
23
|
syncPolicy?: SyncDecisionCallback;
|
|
@@ -43,15 +44,6 @@ export declare class SessionManager {
|
|
|
43
44
|
* lane even when no replay recording is active. Additive, opt-in (default off).
|
|
44
45
|
*/
|
|
45
46
|
private _captureInteractionHeatmap;
|
|
46
|
-
/**
|
|
47
|
-
* Sampling cap for the analytics-lane heatmap emission. Now that capture is
|
|
48
|
-
* ON by default, an unbounded one-event-per-gesture stream could materially
|
|
49
|
-
* inflate ingest volume. We cap emitted interactions per analytics-session
|
|
50
|
-
* window (finalize-scoped per foreground): the first N gestures define the
|
|
51
|
-
* hotspot shape; the long tail is dropped. Resets when the window changes.
|
|
52
|
-
*/
|
|
53
|
-
private _heatmapWindowSessionId;
|
|
54
|
-
private _heatmapWindowCount;
|
|
55
47
|
private session;
|
|
56
48
|
private active;
|
|
57
49
|
private timeoutTimer;
|
|
@@ -207,6 +199,8 @@ export declare class SessionManager {
|
|
|
207
199
|
traceId?: string;
|
|
208
200
|
source?: JourneyEventSource;
|
|
209
201
|
journeyId?: string;
|
|
202
|
+
/** Original observation time; interaction capture may resolve asynchronously. */
|
|
203
|
+
timestamp?: number;
|
|
210
204
|
}): void;
|
|
211
205
|
/**
|
|
212
206
|
* Anchor a crash in the session context.
|
|
@@ -223,7 +217,10 @@ export declare class SessionManager {
|
|
|
223
217
|
* Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
|
|
224
218
|
* Also triggers frame capture for desktop-initiated recordings.
|
|
225
219
|
*/
|
|
226
|
-
onUserAction(subtype: string, payload?: Record<string, unknown
|
|
220
|
+
onUserAction(subtype: string, payload?: Record<string, unknown>, context?: {
|
|
221
|
+
screen?: string;
|
|
222
|
+
timestamp?: number;
|
|
223
|
+
}): void;
|
|
227
224
|
/**
|
|
228
225
|
* Detailed gesture detection — called by ScaleBunDebugRoot touch handlers.
|
|
229
226
|
* Emits a USER_ACTION event with gesture-specific subtype and payload.
|
|
@@ -263,6 +260,14 @@ export declare class SessionManager {
|
|
|
263
260
|
screenWidth?: number;
|
|
264
261
|
screenHeight?: number;
|
|
265
262
|
platform?: string;
|
|
263
|
+
interactionId?: string;
|
|
264
|
+
interactionProtocol?: number;
|
|
265
|
+
occurredAt?: number;
|
|
266
|
+
ui?: string;
|
|
267
|
+
stateStatus?: InteractionStateStatus;
|
|
268
|
+
targetId?: string;
|
|
269
|
+
screenName?: string;
|
|
270
|
+
emitAutomaticAnalytics?: boolean;
|
|
266
271
|
}): void;
|
|
267
272
|
/**
|
|
268
273
|
* Manual frame capture — triggered by Desktop "Capture Step" button.
|
|
@@ -82,6 +82,33 @@ declare class ScaleBunFacade {
|
|
|
82
82
|
* than letting the app discover it as a silent no-update condition.
|
|
83
83
|
*/
|
|
84
84
|
private _maybeStartOta;
|
|
85
|
+
/** Guards against overlapping config-driven OTA checks. */
|
|
86
|
+
private _otaCheckInFlight;
|
|
87
|
+
/** Wall clock of the last config-driven check, for the foreground floor. */
|
|
88
|
+
private _otaLastCheckAt;
|
|
89
|
+
private _otaForegroundListener;
|
|
90
|
+
/**
|
|
91
|
+
* Minimum gap between config-driven checks.
|
|
92
|
+
*
|
|
93
|
+
* A foreground transition is cheap to trigger — app switchers, permission
|
|
94
|
+
* dialogs and share sheets all produce one — so an unthrottled check would
|
|
95
|
+
* put a request on the hot path every time the user glanced away. Ten
|
|
96
|
+
* minutes is well below any realistic release cadence and well above that
|
|
97
|
+
* noise. A host that wants a check on demand calls `useOtaUpdate().sync()`,
|
|
98
|
+
* which is never throttled.
|
|
99
|
+
*/
|
|
100
|
+
private static readonly OTA_MIN_CHECK_INTERVAL_MS;
|
|
101
|
+
/**
|
|
102
|
+
* Drive OTA checks from init config: once at startup, then on each
|
|
103
|
+
* foreground when `checkOnForeground` is on (the schema default).
|
|
104
|
+
*
|
|
105
|
+
* `appVersion` is resolved from the native bridge rather than asked of the
|
|
106
|
+
* integrator, because it gates the server's `targetAppVersion` semver check
|
|
107
|
+
* — sending a wrong or invented value is worse than sending none, and there
|
|
108
|
+
* is no honest default. If it cannot be resolved, the check is skipped with
|
|
109
|
+
* a warning instead of guessing.
|
|
110
|
+
*/
|
|
111
|
+
private _startOtaChecks;
|
|
85
112
|
private _autoEnableDebug;
|
|
86
113
|
/** Boot the Phase 1 envelope tracking lane if an appId is configured. */
|
|
87
114
|
private _maybeStartEventTracker;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalebun/react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "React Native SDK for ScaleBun",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"@babel/runtime": "^7.25.0",
|
|
122
122
|
"@jridgewell/sourcemap-codec": "1.5.5",
|
|
123
123
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
124
|
-
"@scalebun/cli": "^1.
|
|
124
|
+
"@scalebun/cli": "^1.11.0"
|
|
125
125
|
},
|
|
126
126
|
"codegenConfig": {
|
|
127
127
|
"name": "ScaleBunSpec",
|
|
@@ -154,6 +154,7 @@
|
|
|
154
154
|
"watch": "bob build --watch",
|
|
155
155
|
"test:screens": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/screen-detection.test.ts",
|
|
156
156
|
"test:uistate": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/ui-state.test.ts",
|
|
157
|
-
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json"
|
|
157
|
+
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
158
|
+
"test:protocol": "node --experimental-transform-types --import ./scripts/rn-globals.mjs --import ./scripts/register-ts-ext.mjs --test scripts/interaction-protocol.test.ts"
|
|
158
159
|
}
|
|
159
160
|
}
|
|
@@ -172,7 +172,7 @@ export class EventTracker {
|
|
|
172
172
|
this.started = true;
|
|
173
173
|
if (this.cfg.automaticEventTracking) {
|
|
174
174
|
this.automaticEventsUnsubscribe = subscribeAutomaticEvents((event) => {
|
|
175
|
-
this.track(event.name, event.properties);
|
|
175
|
+
this.track(event.name, event.properties, event.timestamp);
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
if (this.cfg.autoLifecycleEvents) {
|
|
@@ -284,9 +284,9 @@ export class EventTracker {
|
|
|
284
284
|
|
|
285
285
|
// ─── tracking ────────────────────────────────────────────────────────────
|
|
286
286
|
|
|
287
|
-
track(eventName: string, properties?: Record<string, any
|
|
287
|
+
track(eventName: string, properties?: Record<string, any>, timestamp?: number): void {
|
|
288
288
|
try {
|
|
289
|
-
this.enqueue(this.buildEnvelope(eventName, properties));
|
|
289
|
+
this.enqueue(this.buildEnvelope(eventName, properties, timestamp));
|
|
290
290
|
try { this.cfg.onEvent?.(eventName); } catch { /* no-throw */ }
|
|
291
291
|
} catch (err) {
|
|
292
292
|
logger.warn(`[ScaleBun.events] track failed: ${(err as Error)?.message}`);
|
|
@@ -408,7 +408,7 @@ export class EventTracker {
|
|
|
408
408
|
|
|
409
409
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
410
410
|
|
|
411
|
-
private buildEnvelope(eventName: string, properties?: Record<string, any
|
|
411
|
+
private buildEnvelope(eventName: string, properties?: Record<string, any>, timestamp?: number): Envelope {
|
|
412
412
|
const ctx = this.cfg.context ?? {};
|
|
413
413
|
let canonicalSessionId: string | null | undefined;
|
|
414
414
|
try {
|
|
@@ -419,7 +419,7 @@ export class EventTracker {
|
|
|
419
419
|
const env: Envelope = {
|
|
420
420
|
event_id: uuid(),
|
|
421
421
|
event_name: eventName,
|
|
422
|
-
event_time: Date.now(),
|
|
422
|
+
event_time: timestamp ?? Date.now(),
|
|
423
423
|
app_id: this.cfg.appId,
|
|
424
424
|
platform: this.cfg.platform ?? resolveEventPlatform(),
|
|
425
425
|
installation_id: this.installationId,
|
|
@@ -39,6 +39,8 @@ export type AutomaticEventName =
|
|
|
39
39
|
export interface AutomaticEvent {
|
|
40
40
|
name: AutomaticEventName;
|
|
41
41
|
properties: Record<string, unknown>;
|
|
42
|
+
/** Original observation time. Delivery can be delayed while native coordinates resolve. */
|
|
43
|
+
timestamp?: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
type AutomaticEventListener = (event: AutomaticEvent) => void;
|
|
@@ -60,10 +62,12 @@ function compactProperties(
|
|
|
60
62
|
export function emitAutomaticEvent(
|
|
61
63
|
name: AutomaticEventName,
|
|
62
64
|
properties?: Record<string, unknown>,
|
|
65
|
+
timestamp?: number,
|
|
63
66
|
): void {
|
|
64
67
|
const event: AutomaticEvent = {
|
|
65
68
|
name,
|
|
66
69
|
properties: compactProperties(properties),
|
|
70
|
+
timestamp,
|
|
67
71
|
};
|
|
68
72
|
if (listeners.size === 0) {
|
|
69
73
|
pending.push(event);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ScaleBun SDK version. Sent with the session-start envelope so the dashboard
|
|
3
3
|
* can attribute telemetry to the SDK build that produced it.
|
|
4
|
-
* Keep in sync with package.json "version".
|
|
4
|
+
* Keep in sync with package.json "version" — `version.test.ts` fails when they drift.
|
|
5
|
+
*
|
|
6
|
+
* Why the test matters: the 1.11.0 bump missed this line, so the build would have reported itself as
|
|
7
|
+
* 1.10.6. Every "is the release live, and on what share of traffic" question is answered from this
|
|
8
|
+
* value, so a stale one makes a rollout unobservable — which is the exact problem sending an SDK
|
|
9
|
+
* version was introduced to solve.
|
|
5
10
|
*/
|
|
6
|
-
export const SDK_VERSION = '1.
|
|
11
|
+
export const SDK_VERSION = '1.11.1';
|