@scalebun/react-native 1.0.3 → 1.0.4
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/core/crash/CrashStorage.kt +115 -1
- package/android/src/main/java/com/scalebun/core/crash/NativeCrashHandler.kt +14 -0
- package/android/src/main/java/com/scalebun/replaysdk/ReplaySdkModule.kt +25 -9
- package/android/src/main/java/com/scalebun/replaysdk/voice/VoiceTranscriber.kt +68 -2
- package/android/src/main/java/com/scalebun/rn/crash/ScaleBunCrashModule.kt +66 -2
- package/ios/Crash/ScaleBunCrashBridge.mm +6 -0
- package/ios/Crash/ScaleBunCrashHandler.swift +14 -1
- package/ios/Crash/ScaleBunCrashModule.swift +14 -0
- package/ios/Crash/ScaleBunCrashSignalBridge.h +32 -1
- package/ios/Crash/ScaleBunCrashSignalBridge.m +172 -7
- package/ios/Crash/ScaleBunCrashSignalStress.c +137 -0
- package/ios/Crash/ScaleBunCrashStorage.swift +75 -0
- package/lib/commonjs/analytics/EventTracker.js +30 -2
- package/lib/commonjs/analytics/EventTracker.js.map +1 -1
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +19 -14
- package/lib/commonjs/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/commonjs/core/config/schema.js +13 -5
- package/lib/commonjs/core/config/schema.js.map +1 -1
- package/lib/commonjs/core/context/device.js +130 -0
- package/lib/commonjs/core/context/device.js.map +1 -1
- package/lib/commonjs/debug/replayWiring.js +2 -1
- package/lib/commonjs/debug/replayWiring.js.map +1 -1
- package/lib/commonjs/features/bugreport/BugReportFeature.js +112 -0
- package/lib/commonjs/features/bugreport/BugReportFeature.js.map +1 -0
- package/lib/commonjs/features/bugreport/buildPayload.js +97 -0
- package/lib/commonjs/features/bugreport/buildPayload.js.map +1 -0
- package/lib/commonjs/features/crash/CrashReporter.js +25 -1
- package/lib/commonjs/features/crash/CrashReporter.js.map +1 -1
- package/lib/commonjs/features/crash/capture.js +27 -9
- package/lib/commonjs/features/crash/capture.js.map +1 -1
- package/lib/commonjs/features/crash/nativeCrashBridge.js +163 -28
- package/lib/commonjs/features/crash/nativeCrashBridge.js.map +1 -1
- package/lib/commonjs/features/engage/EngagePromptProvider.js +36 -2
- package/lib/commonjs/features/engage/EngagePromptProvider.js.map +1 -1
- package/lib/commonjs/features/engage/EngagePromptView.js +47 -14
- package/lib/commonjs/features/engage/EngagePromptView.js.map +1 -1
- package/lib/commonjs/features/engage/engageTriggerEngine.js +10 -0
- package/lib/commonjs/features/engage/engageTriggerEngine.js.map +1 -1
- package/lib/commonjs/features/engage/engageTypes.js.map +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +2 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js.map +1 -1
- package/lib/commonjs/features/ota/OtaOrchestrator.js +63 -5
- package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/commonjs/features/ota/deviceAttributes.js +48 -0
- package/lib/commonjs/features/ota/deviceAttributes.js.map +1 -0
- package/lib/commonjs/features/ota/geoCountry.js +93 -0
- package/lib/commonjs/features/ota/geoCountry.js.map +1 -0
- package/lib/commonjs/features/ota/useOtaUpdate.js +4 -1
- package/lib/commonjs/features/ota/useOtaUpdate.js.map +1 -1
- package/lib/commonjs/features/performance/PerformanceFeature.js +23 -9
- package/lib/commonjs/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/commonjs/features/performance/collectors/FrameMetricsCollector.js +69 -9
- package/lib/commonjs/features/performance/collectors/FrameMetricsCollector.js.map +1 -1
- package/lib/commonjs/features/performance/transport/PerformanceTransport.js +25 -0
- package/lib/commonjs/features/performance/transport/PerformanceTransport.js.map +1 -1
- package/lib/commonjs/features/session/BackendSessionAdapter.js +128 -11
- package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/commonjs/features/session/JourneyEventPipeline.js +1 -0
- package/lib/commonjs/features/session/JourneyEventPipeline.js.map +1 -1
- package/lib/commonjs/features/session/ReplayCaptureManager.js +46 -0
- package/lib/commonjs/features/session/ReplayCaptureManager.js.map +1 -1
- package/lib/commonjs/features/session/SessionManager.js +42 -12
- package/lib/commonjs/features/session/SessionManager.js.map +1 -1
- package/lib/commonjs/features/session/nativeCapture.js +4 -4
- package/lib/commonjs/features/session/nativeCapture.js.map +1 -1
- package/lib/commonjs/features/session/outboxFrameCapture.js +3 -2
- package/lib/commonjs/features/session/outboxFrameCapture.js.map +1 -1
- package/lib/commonjs/features/session/sessionTypes.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/public/ScaleBunFacade.js +150 -2
- package/lib/commonjs/public/ScaleBunFacade.js.map +1 -1
- package/lib/commonjs/specs/NativeScaleBunCrash.js.map +1 -1
- package/lib/module/analytics/EventTracker.js +29 -1
- package/lib/module/analytics/EventTracker.js.map +1 -1
- package/lib/module/bootstrap/SDKBootstrapper.js +17 -12
- package/lib/module/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/module/core/config/schema.js +13 -5
- package/lib/module/core/config/schema.js.map +1 -1
- package/lib/module/core/context/device.js +119 -0
- package/lib/module/core/context/device.js.map +1 -1
- package/lib/module/debug/replayWiring.js +2 -1
- package/lib/module/debug/replayWiring.js.map +1 -1
- package/lib/module/features/bugreport/BugReportFeature.js +105 -0
- package/lib/module/features/bugreport/BugReportFeature.js.map +1 -0
- package/lib/module/features/bugreport/buildPayload.js +91 -0
- package/lib/module/features/bugreport/buildPayload.js.map +1 -0
- package/lib/module/features/crash/CrashReporter.js +25 -1
- package/lib/module/features/crash/CrashReporter.js.map +1 -1
- package/lib/module/features/crash/capture.js +28 -9
- package/lib/module/features/crash/capture.js.map +1 -1
- package/lib/module/features/crash/nativeCrashBridge.js +161 -28
- package/lib/module/features/crash/nativeCrashBridge.js.map +1 -1
- package/lib/module/features/engage/EngagePromptProvider.js +36 -2
- package/lib/module/features/engage/EngagePromptProvider.js.map +1 -1
- package/lib/module/features/engage/EngagePromptView.js +47 -14
- package/lib/module/features/engage/EngagePromptView.js.map +1 -1
- package/lib/module/features/engage/engageTriggerEngine.js +10 -0
- package/lib/module/features/engage/engageTriggerEngine.js.map +1 -1
- package/lib/module/features/engage/engageTypes.js.map +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +2 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js.map +1 -1
- package/lib/module/features/ota/OtaOrchestrator.js +63 -5
- package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
- package/lib/module/features/ota/deviceAttributes.js +43 -0
- package/lib/module/features/ota/deviceAttributes.js.map +1 -0
- package/lib/module/features/ota/geoCountry.js +86 -0
- package/lib/module/features/ota/geoCountry.js.map +1 -0
- package/lib/module/features/ota/useOtaUpdate.js +4 -1
- package/lib/module/features/ota/useOtaUpdate.js.map +1 -1
- package/lib/module/features/performance/PerformanceFeature.js +23 -9
- package/lib/module/features/performance/PerformanceFeature.js.map +1 -1
- package/lib/module/features/performance/collectors/FrameMetricsCollector.js +70 -10
- package/lib/module/features/performance/collectors/FrameMetricsCollector.js.map +1 -1
- package/lib/module/features/performance/transport/PerformanceTransport.js +25 -0
- package/lib/module/features/performance/transport/PerformanceTransport.js.map +1 -1
- package/lib/module/features/session/BackendSessionAdapter.js +128 -11
- package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
- package/lib/module/features/session/JourneyEventPipeline.js +1 -0
- package/lib/module/features/session/JourneyEventPipeline.js.map +1 -1
- package/lib/module/features/session/ReplayCaptureManager.js +46 -0
- package/lib/module/features/session/ReplayCaptureManager.js.map +1 -1
- package/lib/module/features/session/SessionManager.js +42 -12
- package/lib/module/features/session/SessionManager.js.map +1 -1
- package/lib/module/features/session/nativeCapture.js +4 -5
- package/lib/module/features/session/nativeCapture.js.map +1 -1
- package/lib/module/features/session/outboxFrameCapture.js +3 -2
- package/lib/module/features/session/outboxFrameCapture.js.map +1 -1
- package/lib/module/features/session/sessionTypes.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/public/ScaleBunFacade.js +150 -2
- package/lib/module/public/ScaleBunFacade.js.map +1 -1
- package/lib/module/specs/NativeScaleBunCrash.js.map +1 -1
- package/lib/typescript/analytics/EventTracker.d.ts +20 -0
- package/lib/typescript/analytics/EventTracker.d.ts.map +1 -1
- package/lib/typescript/bootstrap/SDKBootstrapper.d.ts.map +1 -1
- package/lib/typescript/core/config/schema.d.ts +1 -1
- package/lib/typescript/core/config/schema.d.ts.map +1 -1
- package/lib/typescript/core/context/device.d.ts +46 -0
- package/lib/typescript/core/context/device.d.ts.map +1 -1
- package/lib/typescript/debug/replayWiring.d.ts.map +1 -1
- package/lib/typescript/features/bugreport/BugReportFeature.d.ts +59 -0
- package/lib/typescript/features/bugreport/BugReportFeature.d.ts.map +1 -0
- package/lib/typescript/features/bugreport/buildPayload.d.ts +51 -0
- package/lib/typescript/features/bugreport/buildPayload.d.ts.map +1 -0
- package/lib/typescript/features/crash/CrashReporter.d.ts +20 -1
- package/lib/typescript/features/crash/CrashReporter.d.ts.map +1 -1
- package/lib/typescript/features/crash/capture.d.ts +21 -2
- package/lib/typescript/features/crash/capture.d.ts.map +1 -1
- package/lib/typescript/features/crash/nativeCrashBridge.d.ts +16 -3
- package/lib/typescript/features/crash/nativeCrashBridge.d.ts.map +1 -1
- package/lib/typescript/features/engage/EngagePromptProvider.d.ts.map +1 -1
- package/lib/typescript/features/engage/EngagePromptView.d.ts.map +1 -1
- package/lib/typescript/features/engage/engageTriggerEngine.d.ts.map +1 -1
- package/lib/typescript/features/engage/engageTypes.d.ts +15 -0
- package/lib/typescript/features/engage/engageTypes.d.ts.map +1 -1
- package/lib/typescript/features/journey/ScaleBunDebugRoot.d.ts.map +1 -1
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +8 -2
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
- package/lib/typescript/features/ota/deviceAttributes.d.ts +18 -0
- package/lib/typescript/features/ota/deviceAttributes.d.ts.map +1 -0
- package/lib/typescript/features/ota/geoCountry.d.ts +14 -0
- package/lib/typescript/features/ota/geoCountry.d.ts.map +1 -0
- package/lib/typescript/features/ota/useOtaUpdate.d.ts +20 -2
- package/lib/typescript/features/ota/useOtaUpdate.d.ts.map +1 -1
- package/lib/typescript/features/performance/PerformanceFeature.d.ts +0 -1
- package/lib/typescript/features/performance/PerformanceFeature.d.ts.map +1 -1
- package/lib/typescript/features/performance/collectors/FrameMetricsCollector.d.ts +33 -0
- package/lib/typescript/features/performance/collectors/FrameMetricsCollector.d.ts.map +1 -1
- package/lib/typescript/features/performance/transport/PerformanceTransport.d.ts.map +1 -1
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts +42 -2
- package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +2 -0
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts.map +1 -1
- package/lib/typescript/features/session/ReplayCaptureManager.d.ts +38 -1
- package/lib/typescript/features/session/ReplayCaptureManager.d.ts.map +1 -1
- package/lib/typescript/features/session/SessionManager.d.ts.map +1 -1
- package/lib/typescript/features/session/nativeCapture.d.ts +2 -1
- package/lib/typescript/features/session/nativeCapture.d.ts.map +1 -1
- package/lib/typescript/features/session/outboxFrameCapture.d.ts.map +1 -1
- package/lib/typescript/features/session/sessionTypes.d.ts +20 -3
- package/lib/typescript/features/session/sessionTypes.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/public/ScaleBunFacade.d.ts +58 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts.map +1 -1
- package/lib/typescript/specs/NativeScaleBunCrash.d.ts +14 -0
- package/lib/typescript/specs/NativeScaleBunCrash.d.ts.map +1 -1
- package/lib/typescript/vendor/protocol/index.d.ts +17 -17
- package/package.json +1 -1
- package/src/analytics/EventTracker.ts +28 -1
- package/src/bootstrap/SDKBootstrapper.ts +15 -14
- package/src/core/config/schema.ts +13 -5
- package/src/core/context/device.ts +146 -0
- package/src/debug/replayWiring.ts +2 -1
- package/src/features/bugreport/BugReportFeature.ts +128 -0
- package/src/features/bugreport/buildPayload.ts +116 -0
- package/src/features/crash/CrashReporter.ts +40 -1
- package/src/features/crash/capture.ts +36 -9
- package/src/features/crash/nativeCrashBridge.ts +156 -22
- package/src/features/engage/EngagePromptProvider.tsx +48 -2
- package/src/features/engage/EngagePromptView.tsx +51 -15
- package/src/features/engage/engageTriggerEngine.ts +15 -0
- package/src/features/engage/engageTypes.ts +15 -0
- package/src/features/journey/ScaleBunDebugRoot.tsx +2 -1
- package/src/features/ota/OtaOrchestrator.ts +75 -6
- package/src/features/ota/deviceAttributes.ts +46 -0
- package/src/features/ota/geoCountry.ts +85 -0
- package/src/features/ota/useOtaUpdate.ts +20 -2
- package/src/features/performance/PerformanceFeature.ts +22 -9
- package/src/features/performance/collectors/FrameMetricsCollector.ts +72 -10
- package/src/features/performance/transport/PerformanceTransport.ts +18 -0
- package/src/features/session/BackendSessionAdapter.ts +137 -10
- package/src/features/session/JourneyEventPipeline.ts +3 -0
- package/src/features/session/ReplayCaptureManager.ts +53 -1
- package/src/features/session/SessionManager.ts +46 -7
- package/src/features/session/nativeCapture.ts +3 -2
- package/src/features/session/outboxFrameCapture.ts +3 -2
- package/src/features/session/sessionTypes.ts +26 -3
- package/src/index.ts +3 -0
- package/src/public/ScaleBunFacade.ts +153 -2
- package/src/specs/NativeScaleBunCrash.ts +16 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createStorageBackend } from '../../storage/StorageBackend';
|
|
2
|
+
import { logger } from '../../core/logger/internalLogger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Device country via an edge Worker — the workaround for an API that is not
|
|
6
|
+
* behind a geo-stamping CDN.
|
|
7
|
+
*
|
|
8
|
+
* The clean architecture is the API itself resolving `cf-ipcountry` at its own
|
|
9
|
+
* edge (see backend `resolveCountry`) — the device can't forge it and every
|
|
10
|
+
* request carries it. When the API record isn't proxied, that header never
|
|
11
|
+
* exists, so this module asks a Cloudflare Worker (which DOES see the device's
|
|
12
|
+
* edge geo) and attaches the answer to the OTA check payload. Host-supplied
|
|
13
|
+
* country always wins; the server treats a client country as locale-grade, not
|
|
14
|
+
* proof of location.
|
|
15
|
+
*
|
|
16
|
+
* Failure-soft by design: a dead Worker, a timeout, or a bad payload leaves the
|
|
17
|
+
* country unset — an unknown country must render as unknown, never be guessed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Default lookup endpoint. Overridable via {@link setGeoEndpoint}. */
|
|
21
|
+
const DEFAULT_GEO_ENDPOINT = 'https://get-location.imohamadfahmy.workers.dev/';
|
|
22
|
+
|
|
23
|
+
const K_COUNTRY = 'scalebun.ota.geoCountry';
|
|
24
|
+
const K_FETCHED_AT = 'scalebun.ota.geoCountryAt';
|
|
25
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // countries change when people travel — daily is plenty
|
|
26
|
+
const TIMEOUT_MS = 2500;
|
|
27
|
+
|
|
28
|
+
/** Cloudflare's non-countries: Tor exits and internal probes. Unknown, not a place. */
|
|
29
|
+
const NON_COUNTRIES = new Set(['XX', 'T1', 'ZZ']);
|
|
30
|
+
|
|
31
|
+
let endpoint = DEFAULT_GEO_ENDPOINT;
|
|
32
|
+
let memory: string | null = null;
|
|
33
|
+
let inFlight: Promise<void> | null = null;
|
|
34
|
+
const storage = createStorageBackend();
|
|
35
|
+
|
|
36
|
+
/** Override the lookup endpoint (e.g. a self-hosted Worker). */
|
|
37
|
+
export function setGeoEndpoint(url: string): void {
|
|
38
|
+
endpoint = url;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Synchronous read of the cached country (uppercase ISO 3166-1 alpha-2), or
|
|
43
|
+
* null when no fresh lookup has ever succeeded. Never triggers network.
|
|
44
|
+
*/
|
|
45
|
+
export function getDeviceCountry(): string | null {
|
|
46
|
+
if (memory) return memory;
|
|
47
|
+
const stored = storage.get(K_COUNTRY);
|
|
48
|
+
const at = parseInt(storage.get(K_FETCHED_AT) ?? '0', 10) || 0;
|
|
49
|
+
if (stored && Date.now() - at < TTL_MS) {
|
|
50
|
+
memory = stored;
|
|
51
|
+
return stored;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refresh the cache from the Worker if stale. Fire-and-forget: call it at
|
|
58
|
+
* orchestrator init; the first check of a cold install may go out without a
|
|
59
|
+
* country (the server stores the country per device on later checks anyway).
|
|
60
|
+
*/
|
|
61
|
+
export function prefetchDeviceCountry(): Promise<void> {
|
|
62
|
+
if (getDeviceCountry()) return Promise.resolve(); // fresh — nothing to do
|
|
63
|
+
if (inFlight) return inFlight;
|
|
64
|
+
inFlight = (async () => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(endpoint, { signal: controller.signal });
|
|
69
|
+
if (!res.ok) return;
|
|
70
|
+
const body = (await res.json()) as { country?: unknown };
|
|
71
|
+
const raw = typeof body?.country === 'string' ? body.country.trim().toUpperCase() : '';
|
|
72
|
+
if (!/^[A-Z]{2}$/.test(raw) || NON_COUNTRIES.has(raw)) return;
|
|
73
|
+
memory = raw;
|
|
74
|
+
storage.set(K_COUNTRY, raw);
|
|
75
|
+
storage.set(K_FETCHED_AT, String(Date.now()));
|
|
76
|
+
logger.debug(`[OTA] Device country resolved: ${raw}`);
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
logger.debug(`[OTA] Geo lookup skipped: ${err?.message ?? err}`);
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
inFlight = null;
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
return inFlight;
|
|
85
|
+
}
|
|
@@ -7,7 +7,22 @@ export interface UseOtaUpdateOptions {
|
|
|
7
7
|
apiUrl: string;
|
|
8
8
|
clientKey: string;
|
|
9
9
|
appVersion: string;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Omit to use the SDK's canonical per-install id (recommended). Supplying a
|
|
12
|
+
* different id here — the old docs suggested `deviceId` — put OTA telemetry
|
|
13
|
+
* in its own identity namespace, disconnected from every other SDK lane.
|
|
14
|
+
*/
|
|
15
|
+
installationId?: string;
|
|
16
|
+
/** OTA channel to pull from. Omitted means `default`. */
|
|
17
|
+
channelName?: string;
|
|
18
|
+
/** ISO 3166-1 alpha-2. The server resolves this from the edge when absent. */
|
|
19
|
+
country?: string;
|
|
20
|
+
/** Free-form device attributes evaluated by ATTRIBUTE targeting rules. */
|
|
21
|
+
attributes?: Record<string, string | number>;
|
|
22
|
+
/** CDP segment ids this installation belongs to. */
|
|
23
|
+
segmentIds?: string[];
|
|
24
|
+
/** Lifecycle stage evaluated by LIFECYCLE_STAGE targeting rules. */
|
|
25
|
+
lifecycleStage?: string;
|
|
11
26
|
autoRestart?: boolean;
|
|
12
27
|
/**
|
|
13
28
|
* When true and the server says `isMandatory`, the hook sets
|
|
@@ -49,7 +64,10 @@ export interface UseOtaUpdateReturn {
|
|
|
49
64
|
* apiUrl: 'https://api.scalebun.com/api/v1',
|
|
50
65
|
* clientKey: 'scalebun_ck_…',
|
|
51
66
|
* appVersion: '1.0.0',
|
|
52
|
-
* installationId
|
|
67
|
+
* // installationId is resolved from the SDK's own persisted install id —
|
|
68
|
+
* // do NOT pass deviceId here (the old example did; it split OTA telemetry
|
|
69
|
+
* // into a separate identity namespace from every other lane).
|
|
70
|
+
* channelName: 'production',
|
|
53
71
|
* mandatoryBlocksUi: true,
|
|
54
72
|
* });
|
|
55
73
|
* ```
|
|
@@ -71,7 +71,6 @@ export class PerformanceFeature implements IFeature {
|
|
|
71
71
|
private static readonly CAPTURE_DELAYS = [300, 600, 1200];
|
|
72
72
|
|
|
73
73
|
// Track event function from SDK pipeline
|
|
74
|
-
private trackFn: ((name: string, payload?: Record<string, unknown>) => void) | null = null;
|
|
75
74
|
|
|
76
75
|
constructor(config?: Partial<PerformanceConfig>) {
|
|
77
76
|
this.config = mergePerformanceConfig(config);
|
|
@@ -208,7 +207,6 @@ export class PerformanceFeature implements IFeature {
|
|
|
208
207
|
async initialize(context: FeatureContext): Promise<void> {
|
|
209
208
|
if (this.active || !this.config.enabled) return;
|
|
210
209
|
|
|
211
|
-
this.trackFn = context.trackEvent;
|
|
212
210
|
this.active = true;
|
|
213
211
|
|
|
214
212
|
// Activate native performance collectors (Android AppStart/Frame/ANR,
|
|
@@ -549,13 +547,28 @@ export class PerformanceFeature implements IFeature {
|
|
|
549
547
|
// Send to debug transport
|
|
550
548
|
this.transport.sendEvent(event);
|
|
551
549
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
550
|
+
/**
|
|
551
|
+
* REMOVED: a duplicate `$perf_event` through the analytics/event pipeline.
|
|
552
|
+
*
|
|
553
|
+
* This claimed to be "for backend ingestion", but the `queuePerformanceMetric` call below is
|
|
554
|
+
* the backend ingestion path — it writes the dedicated `performance_metrics` table, and that
|
|
555
|
+
* is what EVERY consumer reads: `perf-aggregate.ch.ts` (FPS, frameMetricsSessions), the
|
|
556
|
+
* Monitor low-FPS KPI, the page-load RenderingPanel and the Replay Inspector's FPS readout.
|
|
557
|
+
* Nothing in the backend or the dashboard reads `$perf_event` — verified by search.
|
|
558
|
+
*
|
|
559
|
+
* So this line wrote every perf sample a SECOND time, into the event lane, where it landed as
|
|
560
|
+
* `USER_ACTION` and was counted as a user interaction. Measured in production:
|
|
561
|
+
*
|
|
562
|
+
* performance_metrics.frame_metrics 75,278 ← read by everything
|
|
563
|
+
* replay_events (as USER_ACTION) 47,019 ← read by nothing
|
|
564
|
+
*
|
|
565
|
+
* `USER_ACTION` was 99% of all mobile events (62,026 of ~62,600), and only 980 of those
|
|
566
|
+
* carried real gesture data. One session showed 4,735 events against SIX real gestures. That
|
|
567
|
+
* made session event counts meaningless and polluted anything counting user interactions.
|
|
568
|
+
*
|
|
569
|
+
* Dropping it loses no data and no feature — the perf lane's copy is the more complete one —
|
|
570
|
+
* and it costs a duplicate serialize + upload per sample on every session.
|
|
571
|
+
*/
|
|
559
572
|
|
|
560
573
|
// Queue to SaaS backend dedicated performance endpoint
|
|
561
574
|
const backendTransport = SessionManager.getExistingInstance()?.getBackendTransport();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Emits FrameMetricsEvent for transport
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { NativeEventEmitter } from 'react-native';
|
|
14
|
+
import { AppState, NativeEventEmitter, type NativeEventSubscription } from 'react-native';
|
|
15
15
|
import { getNativePerformanceModule } from '../getNativePerformanceModule';
|
|
16
16
|
import type { FrameMetricsEvent, PerfContext } from '../models';
|
|
17
17
|
import { PERF_SCHEMA_VERSION } from '../models';
|
|
@@ -25,6 +25,10 @@ export class FrameMetricsCollector {
|
|
|
25
25
|
private contextProvider: () => PerfContext;
|
|
26
26
|
private eventSubscription: any = null;
|
|
27
27
|
private running = false;
|
|
28
|
+
/** AppState subscription — see `_installAppStateGate`. */
|
|
29
|
+
private appStateSubscription: NativeEventSubscription | null = null;
|
|
30
|
+
/** True while the native sampler is actually running (i.e. armed AND foregrounded). */
|
|
31
|
+
private sampling = false;
|
|
28
32
|
|
|
29
33
|
constructor(
|
|
30
34
|
config: PerformanceConfig,
|
|
@@ -56,22 +60,64 @@ export class FrameMetricsCollector {
|
|
|
56
60
|
},
|
|
57
61
|
);
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
perfModule.startFrameMetrics(this.config.reportIntervalMs);
|
|
62
|
-
}
|
|
63
|
+
this._startNativeSampler();
|
|
64
|
+
this._installAppStateGate();
|
|
63
65
|
} catch {
|
|
64
66
|
// Native module not available — graceful degradation
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Pause sampling while the app is backgrounded.
|
|
72
|
+
*
|
|
73
|
+
* THE BUG THIS FIXES. `startFrameMetrics` hands off to a native loop that runs until something
|
|
74
|
+
* calls `stopFrameMetrics`. Nothing did. The collector had no AppState awareness at all — unlike
|
|
75
|
+
* `AutoScreenLoadCollector`, which has subscribed to it since it was written — so once a session
|
|
76
|
+
* was sampled in, it kept sampling for as long as the PROCESS lived, not as long as the app was
|
|
77
|
+
* being used.
|
|
78
|
+
*
|
|
79
|
+
* Measured in production: sessions with `durationMs` of 86,400,000 (exactly 24h) and 70,797,000
|
|
80
|
+
* (~19.7h) carrying 39,766 and 3,709 frame samples against 0 and 6 real gestures respectively.
|
|
81
|
+
* `frame_metrics` was 75% of ALL mobile events for this reason.
|
|
82
|
+
*
|
|
83
|
+
* The `tier2SampleRate` knob does not help here: it gates WHICH sessions collect, not how long
|
|
84
|
+
* they collect for. A sampled-in session that gets backgrounded still samples for hours, which is
|
|
85
|
+
* why lowering it would have hidden the symptom while leaving the cause.
|
|
86
|
+
*
|
|
87
|
+
* It is also a DATA-QUALITY fix, not only a volume one. Frame timings from a backgrounded app
|
|
88
|
+
* describe a screen nobody is looking at, and they were feeding the low-FPS KPI and jank
|
|
89
|
+
* detection — so the noise was not merely stored, it was being reported as UI performance.
|
|
90
|
+
*
|
|
91
|
+
* NOTE: on iOS the OS already stops delivering CADisplayLink callbacks on background, so the
|
|
92
|
+
* practical effect is largest on Android. Gating in JS keeps the two platforms behaving the same
|
|
93
|
+
* either way, rather than relying on an OS side effect that is not part of any contract.
|
|
94
|
+
*/
|
|
95
|
+
private _installAppStateGate(): void {
|
|
96
|
+
if (this.appStateSubscription) return;
|
|
97
|
+
this.appStateSubscription = AppState.addEventListener('change', (next) => {
|
|
98
|
+
if (!this.running) return; // stop() already tore us down
|
|
99
|
+
if (next === 'active') this._startNativeSampler();
|
|
100
|
+
else this._stopNativeSampler(); // 'background' and 'inactive' (iOS call/app-switcher)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Idempotent — AppState can fire 'active' more than once without an intervening background. */
|
|
105
|
+
private _startNativeSampler(): void {
|
|
106
|
+
if (this.sampling) return;
|
|
107
|
+
try {
|
|
108
|
+
const perfModule = getNativePerformanceModule();
|
|
109
|
+
if (typeof perfModule?.startFrameMetrics === 'function') {
|
|
110
|
+
perfModule.startFrameMetrics(this.config.reportIntervalMs);
|
|
111
|
+
this.sampling = true;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// No-throw: a perf collector must never break the host app.
|
|
73
115
|
}
|
|
116
|
+
}
|
|
74
117
|
|
|
118
|
+
private _stopNativeSampler(): void {
|
|
119
|
+
if (!this.sampling) return;
|
|
120
|
+
this.sampling = false;
|
|
75
121
|
try {
|
|
76
122
|
const perfModule = getNativePerformanceModule();
|
|
77
123
|
if (typeof perfModule?.stopFrameMetrics === 'function') {
|
|
@@ -82,6 +128,22 @@ export class FrameMetricsCollector {
|
|
|
82
128
|
}
|
|
83
129
|
}
|
|
84
130
|
|
|
131
|
+
stop(): void {
|
|
132
|
+
this.running = false;
|
|
133
|
+
if (this.eventSubscription) {
|
|
134
|
+
this.eventSubscription.remove();
|
|
135
|
+
this.eventSubscription = null;
|
|
136
|
+
}
|
|
137
|
+
// Remove the AppState listener BEFORE stopping the sampler: a 'change' arriving mid-teardown
|
|
138
|
+
// would otherwise restart the native loop on a collector that is being shut down, which is the
|
|
139
|
+
// same leak this gate exists to close.
|
|
140
|
+
if (this.appStateSubscription) {
|
|
141
|
+
this.appStateSubscription.remove();
|
|
142
|
+
this.appStateSubscription = null;
|
|
143
|
+
}
|
|
144
|
+
this._stopNativeSampler();
|
|
145
|
+
}
|
|
146
|
+
|
|
85
147
|
private _handleNativeFrameMetrics(data: any): void {
|
|
86
148
|
if (!data || typeof data.totalFrames !== 'number') return;
|
|
87
149
|
|
|
@@ -68,6 +68,24 @@ export class PerformanceTransport {
|
|
|
68
68
|
|
|
69
69
|
/** Send a metric value (for backward compat with existing metric handler) */
|
|
70
70
|
sendMetric(name: string, value: number, unit?: string, tags?: Record<string, string>): void {
|
|
71
|
+
// D4: the desktop sink is only wired under enableDebug(), so in a
|
|
72
|
+
// production build everything through here — JS stall stats, launch
|
|
73
|
+
// timings, and the PUBLIC ScaleBun.sendMetric() API — was buffered into
|
|
74
|
+
// a bounded array and discarded. Mirror every metric onto the backend
|
|
75
|
+
// ingestion lane (the one that provably lands rows); the desktop sink
|
|
76
|
+
// remains the dev-tooling fan-out below.
|
|
77
|
+
try {
|
|
78
|
+
// Lazy import avoids a static cycle: session → performance → session.
|
|
79
|
+
const { SessionManager } = require('../../session/SessionManager');
|
|
80
|
+
const backend = SessionManager.getExistingInstance()?.getBackendTransport();
|
|
81
|
+
backend?.queuePerformanceMetric({
|
|
82
|
+
type: name,
|
|
83
|
+
value,
|
|
84
|
+
metadata: { ...(unit ? { unit } : {}), ...(tags ?? {}) },
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
// Backend lane unavailable (no session yet) — dev sink may still get it.
|
|
88
|
+
}
|
|
71
89
|
const msg = {
|
|
72
90
|
type: 'metric' as const,
|
|
73
91
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -258,6 +258,7 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
258
258
|
bridgeAdapter.getDeviceInfo()
|
|
259
259
|
.then(nativeInfo => {
|
|
260
260
|
const m = session.metadata;
|
|
261
|
+
this._cachedAppVersion = nativeInfo?.appVersion ?? m?.appVersion ?? null;
|
|
261
262
|
const payload = {
|
|
262
263
|
sessionId: session.sessionId,
|
|
263
264
|
deviceId: session.deviceId,
|
|
@@ -394,6 +395,7 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
394
395
|
try {
|
|
395
396
|
const nativeInfo = await bridgeAdapter.getDeviceInfo();
|
|
396
397
|
const m = session.metadata;
|
|
398
|
+
this._cachedAppVersion = nativeInfo?.appVersion ?? m?.appVersion ?? null;
|
|
397
399
|
const payload = {
|
|
398
400
|
sessionId: session.sessionId,
|
|
399
401
|
deviceId: session.deviceId,
|
|
@@ -639,18 +641,56 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
639
641
|
}
|
|
640
642
|
}
|
|
641
643
|
|
|
644
|
+
// D8/D9/D12: this signature was {type, message, stack, screenName} — four
|
|
645
|
+
// fields — while the backend row and every dashboard surface were already
|
|
646
|
+
// built for the rest. The consequences of the four-field bottleneck:
|
|
647
|
+
// regression detection permanently false (no appVersion on any mobile
|
|
648
|
+
// error), Issue.primaryPlatform NULL for every RN crash (no platform), and
|
|
649
|
+
// native crash signal/thread/exception-type discarded one call before the
|
|
650
|
+
// wire (no metadata). The receiver needed nothing; only this signature did.
|
|
651
|
+
/** App version resolved at session start; errors stamp it so regression
|
|
652
|
+
* detection has a version to work with (D8). Null until a session starts. */
|
|
653
|
+
private _cachedAppVersion: string | null = null;
|
|
654
|
+
|
|
655
|
+
private _appVersion(): string | undefined {
|
|
656
|
+
return this._cachedAppVersion ?? undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
642
659
|
queueError(data: {
|
|
643
660
|
type: string; message: string; stack?: string; screenName?: string;
|
|
661
|
+
platform?: string;
|
|
662
|
+
appVersion?: string;
|
|
663
|
+
isFatal?: boolean;
|
|
664
|
+
metadata?: Record<string, unknown>;
|
|
665
|
+
/** Target session. Set ONLY by a recovered native crash; absent means the current session. */
|
|
666
|
+
sessionId?: string;
|
|
667
|
+
/** Stable idempotency key. Set ONLY by a recovered crash; the backend dedupes on it. */
|
|
668
|
+
clientId?: string;
|
|
644
669
|
}): void {
|
|
645
670
|
if (this.destroyed || !this.config.clientKey) return;
|
|
646
|
-
this.pendingErrors.push({
|
|
671
|
+
this.pendingErrors.push({
|
|
672
|
+
...data,
|
|
673
|
+
// The session already knows the app version — never make a crash
|
|
674
|
+
// reporter's callers supply what the transport can fill itself.
|
|
675
|
+
appVersion: data.appVersion ?? this._appVersion(),
|
|
676
|
+
// A recovered crash brings its OWN clientId — the stable id written when the crash was
|
|
677
|
+
// first persisted. Generating a fresh one here would mint a new identity on every retry
|
|
678
|
+
// and defeat the unique constraint the backend dedupes with.
|
|
679
|
+
clientId: data.clientId ?? this._clientId(),
|
|
680
|
+
timestamp: new Date().toISOString(),
|
|
681
|
+
});
|
|
647
682
|
if (this.pendingErrors.length >= 10) {
|
|
648
683
|
this._flushErrors().catch(() => {});
|
|
649
684
|
}
|
|
650
685
|
}
|
|
651
686
|
|
|
652
|
-
/**
|
|
653
|
-
|
|
687
|
+
/**
|
|
688
|
+
* Public: force-flush queued errors immediately (handled-error delivery).
|
|
689
|
+
*
|
|
690
|
+
* @returns whether the batch was DURABLY accepted. Crash recovery deletes its on-disk record on
|
|
691
|
+
* this answer, so it must reflect the backend and not merely that the call returned.
|
|
692
|
+
*/
|
|
693
|
+
flushErrors(): Promise<boolean> {
|
|
654
694
|
return this._flushErrors();
|
|
655
695
|
}
|
|
656
696
|
|
|
@@ -724,13 +764,23 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
724
764
|
private async _enqueueNative(
|
|
725
765
|
lane: 'events' | 'network' | 'errors' | 'performance' | 'logs',
|
|
726
766
|
records: Array<Record<string, unknown>>,
|
|
767
|
+
/**
|
|
768
|
+
* Session to enqueue UNDER. Defaults to the current one, which is right for every live lane.
|
|
769
|
+
*
|
|
770
|
+
* A recovered native crash must override it. Without this parameter the outbox stamped
|
|
771
|
+
* `currentSessionId` on everything, so with the outbox enabled a recovered crash was filed
|
|
772
|
+
* against the session created after the restart — the exact defect the routing above exists
|
|
773
|
+
* to prevent, reappearing through a path that routing never touched.
|
|
774
|
+
*/
|
|
775
|
+
targetSessionId?: string,
|
|
727
776
|
): Promise<boolean> {
|
|
728
|
-
|
|
777
|
+
const sessionId = targetSessionId || this.currentSessionId;
|
|
778
|
+
if (!this.nativeOutboxEnabled || !sessionId) return false;
|
|
729
779
|
const native = getReplaySdkNative();
|
|
730
780
|
if (!native?.enqueueOutboxRecords) return false;
|
|
731
781
|
if (!(await this._ensureNativeOutbox(native))) return false;
|
|
732
782
|
try {
|
|
733
|
-
return await native.enqueueOutboxRecords(lane,
|
|
783
|
+
return await native.enqueueOutboxRecords(lane, sessionId, records);
|
|
734
784
|
} catch {
|
|
735
785
|
return false;
|
|
736
786
|
}
|
|
@@ -852,6 +902,17 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
852
902
|
if (screen) out.screenName = screen;
|
|
853
903
|
const label = e.subtype ?? e.type;
|
|
854
904
|
if (label && label !== e.type) out.label = label;
|
|
905
|
+
/**
|
|
906
|
+
* THIS MAPPER ENUMERATES. Every field the wire carries has to be named here, so a field
|
|
907
|
+
* added to JourneyEvent and not added here is dropped silently — no error, no warning, and
|
|
908
|
+
* nothing downstream can tell the difference between "the SDK did not send it" and "the
|
|
909
|
+
* user never produced it". `frameId` was exactly that case: the column, the DTO field and
|
|
910
|
+
* the ingest write all existed already, and the value died on this line.
|
|
911
|
+
*
|
|
912
|
+
* Omitted when absent rather than sent as null, matching how screenName and label behave
|
|
913
|
+
* above — the v2 lean shape drops empty fields and the server reconstructs them.
|
|
914
|
+
*/
|
|
915
|
+
if (e.frameId) out.frameId = e.frameId;
|
|
855
916
|
const data = this._pruneEmpty(e.payload);
|
|
856
917
|
if (data && Object.keys(data).length > 0) out.data = data;
|
|
857
918
|
return out;
|
|
@@ -1265,15 +1326,81 @@ export class BackendSessionAdapter implements SessionTransport {
|
|
|
1265
1326
|
}
|
|
1266
1327
|
}
|
|
1267
1328
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1329
|
+
/**
|
|
1330
|
+
* D11: deliver a bug report to the SAME ingest the web SDK uses
|
|
1331
|
+
* (`POST /ingestion/sessions/:id/bug-reports`). Fire-and-forget with one
|
|
1332
|
+
* retry deferral: RN previously had NO path to this endpoint at all —
|
|
1333
|
+
* `reportBug()` emitted an analytics event no handler consumed, so the
|
|
1334
|
+
* bug-reports page was empty forever for every RN app.
|
|
1335
|
+
*/
|
|
1336
|
+
submitBugReport(report: { message: string; title?: string; email?: string }): void {
|
|
1337
|
+
if (this.destroyed || !this.config.clientKey || !this.currentSessionId) return;
|
|
1338
|
+
void this._post(
|
|
1339
|
+
`/ingestion/sessions/${this.currentSessionId}/bug-reports`,
|
|
1340
|
+
{ ...report, timestamp: Date.now() },
|
|
1341
|
+
).catch(() => {
|
|
1342
|
+
/* best-effort; the $bug_report analytics breadcrumb still records intent */
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* @returns TRUE only when every group in this batch was DURABLY accepted — an HTTP 2xx, or a
|
|
1348
|
+
* handoff to the native outbox, which is itself a persistent retrying queue.
|
|
1349
|
+
*
|
|
1350
|
+
* The return value is not cosmetic: `drainNativeCrash` deletes the on-disk crash record on the
|
|
1351
|
+
* strength of it. This method used to return `void` and swallow its own errors in the catch
|
|
1352
|
+
* below, so "the promise resolved" meant nothing at all — a failed POST looked exactly like a
|
|
1353
|
+
* successful one, and the caller would have deleted the only durable copy of a crash that never
|
|
1354
|
+
* arrived. Anything less than a real acknowledgement here reintroduces the loss this whole
|
|
1355
|
+
* change exists to prevent.
|
|
1356
|
+
*/
|
|
1357
|
+
private async _flushErrors(): Promise<boolean> {
|
|
1358
|
+
if (this.pendingErrors.length === 0 || !this.currentSessionId || !this.config.clientKey) return false;
|
|
1270
1359
|
const batch = this.pendingErrors.splice(0, 20);
|
|
1271
1360
|
try {
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1361
|
+
if (!this._sessionRowReady()) { this.pendingErrors.unshift(...batch); return false; }
|
|
1362
|
+
/**
|
|
1363
|
+
* ROUTED BY THE ERROR'S OWN SESSION, not by whichever session is current.
|
|
1364
|
+
*
|
|
1365
|
+
* A recovered native crash carries the id of the session it actually happened in — that
|
|
1366
|
+
* process is gone, so the current session is the wrong owner. Sending it under
|
|
1367
|
+
* `currentSessionId` is what marked a healthy session crashed and left the crashed one
|
|
1368
|
+
* looking clean.
|
|
1369
|
+
*
|
|
1370
|
+
* The URL still carries a session because that is the established ingest shape; the
|
|
1371
|
+
* error's own `sessionId` simply chooses WHICH one. Errors are grouped by their target
|
|
1372
|
+
* so a batch mixing a recovered crash with live errors cannot drag the live ones onto
|
|
1373
|
+
* the dead session.
|
|
1374
|
+
*/
|
|
1375
|
+
const byTarget = new Map<string, typeof batch>();
|
|
1376
|
+
for (const e of batch) {
|
|
1377
|
+
const target = (e as { sessionId?: string }).sessionId || this.currentSessionId;
|
|
1378
|
+
byTarget.set(target, [...(byTarget.get(target) ?? []), e]);
|
|
1379
|
+
}
|
|
1380
|
+
let allAccepted = true;
|
|
1381
|
+
for (const [target, errors] of byTarget) {
|
|
1382
|
+
// `sessionId` is a routing instruction, not payload — strip it so the stored row is
|
|
1383
|
+
// byte-identical to one sent the ordinary way.
|
|
1384
|
+
const cleaned = errors.map(({ sessionId: _s, ...rest }: any) => rest);
|
|
1385
|
+
|
|
1386
|
+
// The outbox is enqueued PER TARGET, so a recovered crash keeps its own session.
|
|
1387
|
+
if (await this._enqueueNative('errors', cleaned, target)) continue;
|
|
1388
|
+
|
|
1389
|
+
try {
|
|
1390
|
+
await this._post(ENDPOINTS.INGESTION_ERRORS(target), { errors: cleaned });
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
allAccepted = false;
|
|
1393
|
+
// A permanent error (4xx) will never succeed, so re-queuing it would spin
|
|
1394
|
+
// forever; anything else is worth another attempt.
|
|
1395
|
+
if (!BackendSessionAdapter._isPermanentHttpError(err)) {
|
|
1396
|
+
this.pendingErrors.unshift(...errors);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return allAccepted;
|
|
1275
1401
|
} catch (err) {
|
|
1276
1402
|
if (!BackendSessionAdapter._isPermanentHttpError(err)) this.pendingErrors.unshift(...batch);
|
|
1403
|
+
return false;
|
|
1277
1404
|
}
|
|
1278
1405
|
}
|
|
1279
1406
|
|
|
@@ -91,6 +91,8 @@ export class JourneyEventPipeline {
|
|
|
91
91
|
traceId?: string;
|
|
92
92
|
source?: JourneyEventSource;
|
|
93
93
|
journeyId?: string;
|
|
94
|
+
/** Frame that was on screen when this happened. Resolved by the caller; see sessionTypes. */
|
|
95
|
+
frameId?: string;
|
|
94
96
|
},
|
|
95
97
|
): JourneyEvent | null {
|
|
96
98
|
try {
|
|
@@ -120,6 +122,7 @@ export class JourneyEventPipeline {
|
|
|
120
122
|
subtype: opts?.subtype,
|
|
121
123
|
severity: opts?.severity ?? inferSeverity(type),
|
|
122
124
|
screen: opts?.screen,
|
|
125
|
+
frameId: opts?.frameId,
|
|
123
126
|
payload: opts?.payload,
|
|
124
127
|
correlationId: opts?.correlationId,
|
|
125
128
|
traceId: opts?.traceId,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
getFrameRetentionPriority,
|
|
37
37
|
QUALITY_PRESETS,
|
|
38
38
|
} from './sessionTypes';
|
|
39
|
+
import type { PlatformOS } from '../../core/context/device';
|
|
39
40
|
|
|
40
41
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
41
42
|
|
|
@@ -70,7 +71,8 @@ export interface StagedFrameResult {
|
|
|
70
71
|
frameHash: string;
|
|
71
72
|
masked: boolean;
|
|
72
73
|
timestamp: number;
|
|
73
|
-
|
|
74
|
+
// Widened: frames now carry the REAL Platform.OS instead of being relabelled 'android'.
|
|
75
|
+
platform: PlatformOS;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/** JS-authoritative frame metadata written to the outbox on commit. */
|
|
@@ -258,6 +260,15 @@ export class ReplayCaptureManager {
|
|
|
258
260
|
private _lastKeyframeTs = 0;
|
|
259
261
|
/** P2-5: frameId of the last emitted keyframe — delta base reference. */
|
|
260
262
|
private _lastKeyframeId: string | null = null;
|
|
263
|
+
/**
|
|
264
|
+
* Screen the last emitted keyframe was captured on.
|
|
265
|
+
*
|
|
266
|
+
* Tracked alongside the id purely so `currentKeyframe()` can refuse to hand a frame to an
|
|
267
|
+
* event on a different screen. The id and timestamp alone cannot answer that, and a frame
|
|
268
|
+
* from the previous screen is worse than none — it attaches a tap to a photograph of
|
|
269
|
+
* somewhere else, which every layer downstream would then present with full confidence.
|
|
270
|
+
*/
|
|
271
|
+
private _lastKeyframeScreen: string | null = null;
|
|
261
272
|
/** P2-5: emitted frames since the last keyframe (keyframe-interval guard). */
|
|
262
273
|
private _framesSinceKeyframe = 0;
|
|
263
274
|
|
|
@@ -275,6 +286,45 @@ export class ReplayCaptureManager {
|
|
|
275
286
|
|
|
276
287
|
// ─── Public API ─────────────────────────────────────────────────────
|
|
277
288
|
|
|
289
|
+
/**
|
|
290
|
+
* The keyframe that is still showing, or null.
|
|
291
|
+
*
|
|
292
|
+
* Answers "which picture was the user looking at" for an event that is about to be emitted,
|
|
293
|
+
* so a tap can carry the id of the frame it happened ON. Read-only; captures nothing.
|
|
294
|
+
*
|
|
295
|
+
* TWO CONDITIONS, both refusals rather than approximations:
|
|
296
|
+
*
|
|
297
|
+
* SAME SCREEN. A keyframe from the previous screen is not what the user was looking at.
|
|
298
|
+
* Navigation evicts the pending capture rather than re-pointing it, so after a screen
|
|
299
|
+
* change the newest keyframe genuinely belongs to somewhere else.
|
|
300
|
+
*
|
|
301
|
+
* STILL FRESH. A screen mutates constantly — a list loads, a banner rotates, a sheet is
|
|
302
|
+
* dismissed. Past `maxAgeMs` the stored picture stops being evidence of what is on screen,
|
|
303
|
+
* and stale evidence presented as current is the failure mode this whole feature exists to
|
|
304
|
+
* avoid. 10 s is deliberately conservative: the capture manager already photographs on
|
|
305
|
+
* interaction, navigation and scroll settle, so a genuinely active screen refreshes well
|
|
306
|
+
* inside it, and a screen quiet for longer than that is one nobody is interacting with.
|
|
307
|
+
*
|
|
308
|
+
* Returning null is a correct, common answer and downstream MUST treat it as "not captured"
|
|
309
|
+
* rather than as a default.
|
|
310
|
+
*/
|
|
311
|
+
currentKeyframe(
|
|
312
|
+
screenName?: string,
|
|
313
|
+
maxAgeMs = 10_000,
|
|
314
|
+
now: number = Date.now(),
|
|
315
|
+
): { frameId: string; screen: string | null; capturedAt: number } | null {
|
|
316
|
+
if (!this._lastKeyframeId || this._lastKeyframeTs <= 0) return null;
|
|
317
|
+
if (now - this._lastKeyframeTs > maxAgeMs) return null;
|
|
318
|
+
// Compared only when the caller knows its screen. An event with no screen cannot be shown
|
|
319
|
+
// to belong to this frame, so it does not get one.
|
|
320
|
+
if (!screenName || this._lastKeyframeScreen !== screenName) return null;
|
|
321
|
+
return {
|
|
322
|
+
frameId: this._lastKeyframeId,
|
|
323
|
+
screen: this._lastKeyframeScreen,
|
|
324
|
+
capturedAt: this._lastKeyframeTs,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
278
328
|
/** Set callback for when a frame is captured */
|
|
279
329
|
setFrameEmitCallback(cb: FrameEmitCallback): void {
|
|
280
330
|
this.onFrameEmit = cb;
|
|
@@ -291,6 +341,7 @@ export class ReplayCaptureManager {
|
|
|
291
341
|
this._lastFrameHash = null;
|
|
292
342
|
this._lastKeyframeTs = 0;
|
|
293
343
|
this._lastKeyframeId = null;
|
|
344
|
+
this._lastKeyframeScreen = null;
|
|
294
345
|
this._framesSinceKeyframe = 0;
|
|
295
346
|
|
|
296
347
|
// Start frequency timer if in frequency mode
|
|
@@ -808,6 +859,7 @@ export class ReplayCaptureManager {
|
|
|
808
859
|
if (frameType === 'keyframe') {
|
|
809
860
|
this._lastKeyframeId = frameId;
|
|
810
861
|
this._lastKeyframeTs = now;
|
|
862
|
+
this._lastKeyframeScreen = screenName ?? null;
|
|
811
863
|
this._framesSinceKeyframe = 0;
|
|
812
864
|
} else {
|
|
813
865
|
this._framesSinceKeyframe++;
|