@scalebun/react-native 1.0.3 → 1.1.0
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/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +70 -4
- 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 +31 -23
- package/lib/commonjs/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/commonjs/compat/codepush.js +78 -15
- package/lib/commonjs/compat/codepush.js.map +1 -1
- package/lib/commonjs/core/config/schema.js +26 -5
- package/lib/commonjs/core/config/schema.js.map +1 -1
- package/lib/commonjs/core/constants/endpoints.js +9 -6
- package/lib/commonjs/core/constants/endpoints.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 +72 -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 +199 -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 +29 -21
- package/lib/module/bootstrap/SDKBootstrapper.js.map +1 -1
- package/lib/module/compat/codepush.js +77 -14
- package/lib/module/compat/codepush.js.map +1 -1
- package/lib/module/core/config/schema.js +26 -5
- package/lib/module/core/config/schema.js.map +1 -1
- package/lib/module/core/constants/endpoints.js +9 -6
- package/lib/module/core/constants/endpoints.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 +72 -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 +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/public/ScaleBunFacade.js +199 -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/compat/codepush.d.ts +27 -4
- package/lib/typescript/compat/codepush.d.ts.map +1 -1
- package/lib/typescript/core/config/schema.d.ts +4 -1
- package/lib/typescript/core/config/schema.d.ts.map +1 -1
- package/lib/typescript/core/constants/endpoints.d.ts +9 -6
- package/lib/typescript/core/constants/endpoints.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/EngageTransport.d.ts +1 -1
- package/lib/typescript/features/engage/EngageTransport.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 +14 -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 +69 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts.map +1 -1
- package/lib/typescript/public/types.d.ts +34 -8
- package/lib/typescript/public/types.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 +27 -23
- package/src/compat/codepush.ts +87 -16
- package/src/core/config/schema.ts +26 -5
- package/src/core/constants/endpoints.ts +9 -6
- 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/EngageTransport.ts +1 -1
- 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 +84 -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 +4 -1
- package/src/public/ScaleBunFacade.ts +198 -2
- package/src/public/types.ts +35 -8
- package/src/specs/NativeScaleBunCrash.ts +16 -0
|
@@ -110,6 +110,21 @@ export interface EngageCampaignConfig {
|
|
|
110
110
|
storeUrl?: string;
|
|
111
111
|
highRatingThreshold?: number;
|
|
112
112
|
routeLowToFeedback?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Route this campaign's submission to BUG REPORTS instead of Engage responses.
|
|
115
|
+
*
|
|
116
|
+
* Engage responses and bug reports are different products: a survey response measures sentiment
|
|
117
|
+
* in aggregate, while a bug report is a single item that gets a status, a priority and an
|
|
118
|
+
* assignee on /diagnose/bug-reports. A FEEDBACK campaign triggered by `shake_device` is almost
|
|
119
|
+
* always the second kind — someone shook the phone because something is broken.
|
|
120
|
+
*
|
|
121
|
+
* Opt-IN per campaign rather than rerouting every FEEDBACK campaign, because rerouting would
|
|
122
|
+
* silently break existing sentiment surveys, and because the destination should be visible to
|
|
123
|
+
* whoever configures the campaign rather than inferred from its trigger.
|
|
124
|
+
*
|
|
125
|
+
* Absent/false keeps the existing behaviour exactly.
|
|
126
|
+
*/
|
|
127
|
+
fileAsBugReport?: boolean;
|
|
113
128
|
enabled: boolean;
|
|
114
129
|
}
|
|
115
130
|
|
|
@@ -30,6 +30,7 @@ import React, {
|
|
|
30
30
|
import { View, NativeEventEmitter, NativeModules } from 'react-native';
|
|
31
31
|
import { emitAutomaticEvent } from '../../analytics/automaticEvents';
|
|
32
32
|
import { resolveTouchTarget, describeTouchTarget } from './touchTarget';
|
|
33
|
+
import { resolvePlatformOS } from '../../core/context/device';
|
|
33
34
|
|
|
34
35
|
// ─── Lazy imports to avoid native module cascade at load time ────────────────
|
|
35
36
|
// We only import TYPE references at the top level; actual modules are require()'d
|
|
@@ -159,7 +160,7 @@ export function ScaleBunDebugRoot({
|
|
|
159
160
|
byteSize: result.sizeBytes || Math.round(result.base64.length * 0.75),
|
|
160
161
|
format: (result.format ?? 'jpeg') as string,
|
|
161
162
|
frameHash: (result as any).frameHash as string | undefined,
|
|
162
|
-
platform: (
|
|
163
|
+
platform: resolvePlatformOS() as 'android' | 'ios',
|
|
163
164
|
};
|
|
164
165
|
} catch {
|
|
165
166
|
return null;
|
|
@@ -11,10 +11,14 @@ import { noThrow } from '../../core/lifecycle/crashSafe';
|
|
|
11
11
|
import NativeScaleBunOta from '../../specs/NativeScaleBunOta';
|
|
12
12
|
import type { OtaCheckRequest, OtaCheckResponse, OtaBundlePayload } from './OtaTypes';
|
|
13
13
|
import { otaEventEmitter } from './OtaEventEmitter';
|
|
14
|
+
import { getDeviceCountry, prefetchDeviceCountry } from './geoCountry';
|
|
15
|
+
import { deviceTargetingAttributes } from './deviceAttributes';
|
|
16
|
+
import { K_INSTALLATION_ID } from '../../analytics/EventTracker';
|
|
14
17
|
import { verifyBundleSignature, type SignatureConfig } from './signature';
|
|
15
18
|
import { detectOtaEnvironment, type OtaEnvironment } from './environment';
|
|
16
19
|
import { retryWithBackoff, isRetryableNetworkError, HttpStatusError } from './retry';
|
|
17
20
|
import { createStorageBackend, type StorageBackend } from '../../storage/StorageBackend';
|
|
21
|
+
import { resolveMobileOS } from '../../core/context/device';
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Identity marker injected into the bundle by `scalebun ota publish` before
|
|
@@ -67,6 +71,36 @@ function subscribeNativeProgress(
|
|
|
67
71
|
/** POST at most once per this interval; first and terminal ticks always go. */
|
|
68
72
|
const PROGRESS_POST_INTERVAL_MS = 1_000;
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* P8 — the SDK's canonical per-install id, shared with the analytics lane.
|
|
76
|
+
*
|
|
77
|
+
* OTA used to REQUIRE a caller-supplied `installationId`, and the documented
|
|
78
|
+
* example passed `deviceId` — so OTA telemetry lived in its own identity
|
|
79
|
+
* namespace, disconnected from the id every other lane reports. Every join
|
|
80
|
+
* from OTA data to CDP data (blast radius, MRR-at-risk, churn bands) was a
|
|
81
|
+
* cross-namespace join that matched by luck.
|
|
82
|
+
*
|
|
83
|
+
* Reads (or mints, for OTA-only apps that never initialize analytics) the same
|
|
84
|
+
* persisted value `EventTracker` uses, via the same storage backend and the
|
|
85
|
+
* same exported key — one install, one id, every lane.
|
|
86
|
+
*/
|
|
87
|
+
function resolveInstallationId(): string {
|
|
88
|
+
try {
|
|
89
|
+
const storage = createStorageBackend();
|
|
90
|
+
const existing = storage.get(K_INSTALLATION_ID);
|
|
91
|
+
if (existing) return existing;
|
|
92
|
+
const id = `inst-${Math.random().toString(36).slice(2, 10)}${Math.random()
|
|
93
|
+
.toString(36)
|
|
94
|
+
.slice(2, 10)}`;
|
|
95
|
+
storage.set(K_INSTALLATION_ID, id);
|
|
96
|
+
return id;
|
|
97
|
+
} catch {
|
|
98
|
+
// Storage unavailable (tests, exotic hosts): a per-process id keeps sync
|
|
99
|
+
// functional; it will stabilize on the first run where storage works.
|
|
100
|
+
return `inst-ephemeral-${Math.random().toString(36).slice(2, 10)}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
/**
|
|
71
105
|
* Deliver queued OTA lifecycle events to the backend's batch endpoint.
|
|
72
106
|
*
|
|
@@ -93,13 +127,18 @@ async function deliverOtaEvents(params: {
|
|
|
93
127
|
}): Promise<void> {
|
|
94
128
|
const events = otaEventEmitter.flush();
|
|
95
129
|
if (!events.length) return;
|
|
130
|
+
// A platform OTA cannot serve is not reported as Android — it is not reported at all.
|
|
131
|
+
const mobileOS = resolveMobileOS();
|
|
132
|
+
if (!mobileOS) return;
|
|
96
133
|
|
|
97
134
|
const items = events.map((e) => ({
|
|
98
135
|
kind: 'ota_event' as const,
|
|
99
136
|
type: e.type,
|
|
100
137
|
bundleId: e.bundleId,
|
|
101
138
|
installationId: params.installationId,
|
|
102
|
-
platform
|
|
139
|
+
// OTA bundles are compiled per platform, so this genuinely is ios|android. Narrowed via
|
|
140
|
+
// resolveMobileOS so a non-mobile RN target is skipped rather than served an Android bundle.
|
|
141
|
+
platform: mobileOS,
|
|
103
142
|
appVersion: params.appVersion,
|
|
104
143
|
durationMs: e.durationMs,
|
|
105
144
|
errorCode: e.error ?? e.reason,
|
|
@@ -160,6 +199,15 @@ const DEFAULT_MAX_REVERT_ATTEMPTS = 2;
|
|
|
160
199
|
export class OtaOrchestrator {
|
|
161
200
|
private enabled = false;
|
|
162
201
|
private currentBundle: OtaBundlePayload | null = null;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The bundle this process is running, once hydrated from the native slots.
|
|
205
|
+
* Read-only view for integrations (e.g. the CodePush shim's
|
|
206
|
+
* getUpdateMetadata) that need to report the running version.
|
|
207
|
+
*/
|
|
208
|
+
public getCurrentBundle(): OtaBundlePayload | null {
|
|
209
|
+
return this.currentBundle;
|
|
210
|
+
}
|
|
163
211
|
private isRestartRequiredState = false;
|
|
164
212
|
private healthyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
165
213
|
private bootGuardConfig: BootGuardConfig = {};
|
|
@@ -244,6 +292,11 @@ export class OtaOrchestrator {
|
|
|
244
292
|
// no way to know what the device was actually on.
|
|
245
293
|
this.hydrateCurrentBundleFromSlots();
|
|
246
294
|
|
|
295
|
+
// Warm the device-country cache (edge Worker lookup) so checks can carry
|
|
296
|
+
// a country when the API itself sits behind no geo-stamping CDN.
|
|
297
|
+
// Fire-and-forget and failure-soft — a check without a country is valid.
|
|
298
|
+
void prefetchDeviceCountry();
|
|
299
|
+
|
|
247
300
|
// Prove the bundle we installed is the bundle that loaded.
|
|
248
301
|
this.verifyRunningBundleIdentity();
|
|
249
302
|
|
|
@@ -472,15 +525,24 @@ export class OtaOrchestrator {
|
|
|
472
525
|
// never carried it — so every device resolved to `default` no matter what
|
|
473
526
|
// the host app asked for. Undefined fields are omitted by JSON.stringify,
|
|
474
527
|
// so an app that supplies nothing behaves exactly as before.
|
|
528
|
+
// OTA has no bundle for a non-mobile target; claiming Android there would install the wrong one.
|
|
529
|
+
const mobileOS = resolveMobileOS();
|
|
530
|
+
if (!mobileOS) throw new Error('OTA is not supported on this platform');
|
|
475
531
|
const payload: OtaCheckRequest = {
|
|
476
532
|
appVersion: requestParams.appVersion,
|
|
477
|
-
platform:
|
|
533
|
+
platform: mobileOS,
|
|
478
534
|
installationId: requestParams.installationId,
|
|
479
535
|
currentBundleId: this.currentBundle?.id,
|
|
480
536
|
currentBundleHash: this.currentBundle?.sha256,
|
|
481
537
|
channelName: requestParams.channelName,
|
|
482
|
-
country
|
|
483
|
-
|
|
538
|
+
// Host-supplied country wins; otherwise the cached edge-Worker lookup
|
|
539
|
+
// fills in (see geoCountry.ts). Undefined when neither knows — the
|
|
540
|
+
// server renders unknown as unknown rather than guessing.
|
|
541
|
+
country: requestParams.country ?? getDeviceCountry() ?? undefined,
|
|
542
|
+
// Hardware/OS context under the host's own attributes (host wins on
|
|
543
|
+
// collision) — this is what lets ATTRIBUTE rules target device classes
|
|
544
|
+
// (deviceModel / manufacturer / brand / osVersion / formFactor).
|
|
545
|
+
attributes: { ...deviceTargetingAttributes(), ...requestParams.attributes },
|
|
484
546
|
segmentIds: requestParams.segmentIds,
|
|
485
547
|
lifecycleStage: requestParams.lifecycleStage,
|
|
486
548
|
// Runtime compatibility context. Without these the backend cannot tell
|
|
@@ -529,13 +591,29 @@ export class OtaOrchestrator {
|
|
|
529
591
|
* Downloads, stage, and apply an update in one step.
|
|
530
592
|
* Sprint 5: emits lifecycle events + starts boot-guard heartbeat.
|
|
531
593
|
*/
|
|
532
|
-
async sync(
|
|
594
|
+
async sync(rawParams: {
|
|
533
595
|
apiUrl: string;
|
|
534
596
|
clientKey: string;
|
|
535
597
|
appVersion: string;
|
|
536
|
-
|
|
598
|
+
/** Omit to use the SDK's canonical per-install id (recommended — P8). */
|
|
599
|
+
installationId?: string;
|
|
600
|
+
// P1: these five existed on checkForUpdate and on the wire contract, but
|
|
601
|
+
// sync() — the only caller that matters — never accepted them, so they
|
|
602
|
+
// were structurally undefined on every real device: every install pinned
|
|
603
|
+
// to the `default` channel and the whole targeting engine matched nothing.
|
|
604
|
+
// The C1 comment in OtaTypes.ts claimed this was fixed; the plumbing had
|
|
605
|
+
// stopped exactly one function short of true.
|
|
606
|
+
channelName?: string;
|
|
607
|
+
country?: string;
|
|
608
|
+
attributes?: Record<string, string | number>;
|
|
609
|
+
segmentIds?: string[];
|
|
610
|
+
lifecycleStage?: string;
|
|
537
611
|
autoRestart?: boolean;
|
|
538
612
|
}): Promise<SyncResult> {
|
|
613
|
+
const params = {
|
|
614
|
+
...rawParams,
|
|
615
|
+
installationId: rawParams.installationId ?? resolveInstallationId(),
|
|
616
|
+
};
|
|
539
617
|
try {
|
|
540
618
|
if (!this.enabled || !NativeScaleBunOta) {
|
|
541
619
|
return { status: 'ERROR', error: 'OTA native module disabled or missing' };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Dimensions, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hardware/OS context auto-attached to OTA check payloads so ATTRIBUTE targeting
|
|
5
|
+
* rules can address device classes without any host-app wiring:
|
|
6
|
+
*
|
|
7
|
+
* deviceModel — 'SM-A155F', '23129RAA4G'… (Android; iOS does not expose it in JS)
|
|
8
|
+
* manufacturer — 'samsung', 'xiaomi'… (lowercased: rules shouldn't fail on casing)
|
|
9
|
+
* brand — 'redmi' vs manufacturer 'xiaomi' — sub-brands are real segments
|
|
10
|
+
* osVersion — Android Release ('14') / iOS version ('17.5')
|
|
11
|
+
* formFactor — 'phone' | 'tablet' | 'tv' (Android tablets via the 600dp
|
|
12
|
+
* smallest-width convention — the same line AndroidX draws)
|
|
13
|
+
*
|
|
14
|
+
* Host-supplied attributes always win on key collision: an app that knows better
|
|
15
|
+
* (e.g. its own device tiering) must not be overridden by generic detection.
|
|
16
|
+
* Everything here is best-effort — a missing constant yields a missing key,
|
|
17
|
+
* never a guessed value.
|
|
18
|
+
*/
|
|
19
|
+
export function deviceTargetingAttributes(): Record<string, string> {
|
|
20
|
+
const out: Record<string, string> = {};
|
|
21
|
+
try {
|
|
22
|
+
const c = (Platform.constants ?? {}) as Record<string, unknown>;
|
|
23
|
+
if (Platform.OS === 'android') {
|
|
24
|
+
if (typeof c.Model === 'string' && c.Model) out.deviceModel = c.Model;
|
|
25
|
+
if (typeof c.Manufacturer === 'string' && c.Manufacturer) {
|
|
26
|
+
out.manufacturer = c.Manufacturer.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
if (typeof c.Brand === 'string' && c.Brand) out.brand = c.Brand.toLowerCase();
|
|
29
|
+
if (typeof c.Release === 'string' && c.Release) out.osVersion = c.Release;
|
|
30
|
+
const screen = Dimensions.get('screen');
|
|
31
|
+
const smallestDp = Math.min(screen.width, screen.height);
|
|
32
|
+
out.formFactor = Platform.isTV ? 'tv' : smallestDp >= 600 ? 'tablet' : 'phone';
|
|
33
|
+
} else {
|
|
34
|
+
const osVersion = typeof c.osVersion === 'string' ? c.osVersion : String(Platform.Version ?? '');
|
|
35
|
+
if (osVersion) out.osVersion = osVersion;
|
|
36
|
+
out.formFactor = Platform.isTV
|
|
37
|
+
? 'tv'
|
|
38
|
+
: (c as { interfaceIdiom?: string }).interfaceIdiom === 'pad'
|
|
39
|
+
? 'tablet'
|
|
40
|
+
: 'phone';
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Targeting enrichment must never break a check. Missing keys are honest.
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -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,
|