@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
|
@@ -6,11 +6,19 @@ export const ConfigSchema = z.object({
|
|
|
6
6
|
projectId: z.string().min(1).optional(),
|
|
7
7
|
publishableKey: z.string().min(1).optional(),
|
|
8
8
|
/**
|
|
9
|
-
* Backend API base URL.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
* Backend API base URL. LOCKED to {@link DEFAULT_API_BASE_URL} — the ScaleBun
|
|
10
|
+
* cloud endpoint is fixed and cannot be overridden by integrators. Any value
|
|
11
|
+
* supplied here is accepted (no validation error, for backward compatibility)
|
|
12
|
+
* but ignored: the bootstrapper force-overwrites it with DEFAULT_API_BASE_URL
|
|
13
|
+
* immediately after parse. To point the SDK at a different backend, change the
|
|
14
|
+
* constant at build time.
|
|
15
|
+
*/
|
|
16
|
+
// Thunk, not a bare string: `default()` is typed `Partial<T> | (() => T)`, and for
|
|
17
|
+
// `z.any()` T is `any`, so `Partial<any>` widens to an index signature that a string
|
|
18
|
+
// does not satisfy. The function overload does, and DefaultSchema calls it only when
|
|
19
|
+
// the input is undefined — identical at runtime, and it type-checks, which `bob build`
|
|
20
|
+
// requires to emit definitions at all.
|
|
21
|
+
apiBaseUrl: z.any().default(() => DEFAULT_API_BASE_URL),
|
|
14
22
|
debug: z.boolean().default(false),
|
|
15
23
|
/**
|
|
16
24
|
* SDK internal log verbosity. Recoverable transport failures are logged at
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place that answers "what device is this?".
|
|
3
|
+
*
|
|
4
|
+
* It was previously answered three incompatible ways at once:
|
|
5
|
+
*
|
|
6
|
+
* 1. The replay lane asked the native bridge and got the truth.
|
|
7
|
+
* 2. The analytics lane took `ScaleBun.init({ context })` verbatim and shipped whatever the
|
|
8
|
+
* integrator typed. Production carries `device_model: "android"`, `manufacturer: "android"`
|
|
9
|
+
* and `sdk_version: "qa-app-1.0"` because an app hand-wrote them — none of those are a model,
|
|
10
|
+
* a manufacturer, or an SDK version.
|
|
11
|
+
* 3. Seven call sites wrote `Platform.OS === 'ios' ? 'ios' : 'android'`, which reports every
|
|
12
|
+
* non-iOS platform — windows, macos, web — as Android.
|
|
13
|
+
*
|
|
14
|
+
* Detection belongs to the SDK. An integrator cannot be expected to know the device better than the
|
|
15
|
+
* process running on it, and asking them to means the field is wrong whenever they guess.
|
|
16
|
+
*
|
|
17
|
+
* Every value here is measured or absent. Nothing is defaulted to a plausible-looking constant: a
|
|
18
|
+
* fabricated device model is indistinguishable from a real one to whoever reads the dashboard, which
|
|
19
|
+
* is strictly worse than an empty field.
|
|
20
|
+
*/
|
|
21
|
+
import { SDK_VERSION } from '../constants/version';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The real `Platform.OS`, never narrowed.
|
|
25
|
+
*
|
|
26
|
+
* React Native's own union is `'ios' | 'android' | 'windows' | 'macos' | 'web'`, and the protocol's
|
|
27
|
+
* platform field is an open enum precisely so new ones travel without a release. Collapsing that to
|
|
28
|
+
* a two-way ternary is what made every macOS and Windows session read as Android.
|
|
29
|
+
*/
|
|
30
|
+
export type PlatformOS = 'ios' | 'android' | 'windows' | 'macos' | 'web' | 'unknown';
|
|
31
|
+
|
|
32
|
+
interface RNPlatform {
|
|
33
|
+
OS?: string;
|
|
34
|
+
Version?: string | number;
|
|
35
|
+
constants?: {
|
|
36
|
+
Manufacturer?: string;
|
|
37
|
+
Model?: string;
|
|
38
|
+
Brand?: string;
|
|
39
|
+
Release?: string;
|
|
40
|
+
osVersion?: string;
|
|
41
|
+
systemName?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rnPlatform(): RNPlatform | null {
|
|
46
|
+
try {
|
|
47
|
+
return (require('react-native') as { Platform?: RNPlatform }).Platform ?? null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `Platform.OS`, verbatim.
|
|
55
|
+
*
|
|
56
|
+
* `'unknown'` when React Native itself cannot be loaded — the previous `catch { return 'android' }`
|
|
57
|
+
* turned "we could not tell" into a confident, wrong answer.
|
|
58
|
+
*/
|
|
59
|
+
export function resolvePlatformOS(): PlatformOS {
|
|
60
|
+
const os = rnPlatform()?.OS;
|
|
61
|
+
switch (os) {
|
|
62
|
+
case 'ios':
|
|
63
|
+
case 'android':
|
|
64
|
+
case 'windows':
|
|
65
|
+
case 'macos':
|
|
66
|
+
case 'web':
|
|
67
|
+
return os;
|
|
68
|
+
default:
|
|
69
|
+
// A platform React Native gained after this build. Pass it through rather than guess —
|
|
70
|
+
// the wire's platform field is an open enum for exactly this case.
|
|
71
|
+
return (os as PlatformOS) ?? 'unknown';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `'ios' | 'android'` for the few call sites whose payload genuinely only has those two, keeping the
|
|
77
|
+
* narrowing HONEST: `null` where it used to silently say Android.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveMobileOS(): 'ios' | 'android' | null {
|
|
80
|
+
const os = resolvePlatformOS();
|
|
81
|
+
return os === 'ios' || os === 'android' ? os : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The keys this module owns. An integrator-supplied value for any of them is overridden. */
|
|
85
|
+
export const DETECTED_CONTEXT_KEYS = [
|
|
86
|
+
'sdk_version',
|
|
87
|
+
'platform_os',
|
|
88
|
+
'os_version',
|
|
89
|
+
'device_model',
|
|
90
|
+
'manufacturer',
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Everything derivable WITHOUT the native bridge, so the very first event already carries it.
|
|
95
|
+
*
|
|
96
|
+
* The bridge is async and may not be installed at all (Expo Go, an older native binary); waiting on
|
|
97
|
+
* it would leave the opening events of every session unattributed. `Platform.constants` is populated
|
|
98
|
+
* synchronously at startup and covers Android fully — iOS exposes no model there, which is why
|
|
99
|
+
* `device_model` stays absent on iOS until {@link nativeDeviceContext} fills it in.
|
|
100
|
+
*/
|
|
101
|
+
export function staticDeviceContext(): Record<string, string> {
|
|
102
|
+
const platform = rnPlatform();
|
|
103
|
+
const out: Record<string, string> = {
|
|
104
|
+
sdk_version: SDK_VERSION,
|
|
105
|
+
platform_os: resolvePlatformOS(),
|
|
106
|
+
};
|
|
107
|
+
if (!platform) return out;
|
|
108
|
+
|
|
109
|
+
const c = platform.constants ?? {};
|
|
110
|
+
// Android: Release is the marketing version ("14"); Version is the API level (34).
|
|
111
|
+
// iOS: osVersion is the real thing; Platform.Version mirrors it.
|
|
112
|
+
const osVersion = c.Release ?? c.osVersion ?? (platform.Version != null ? String(platform.Version) : undefined);
|
|
113
|
+
if (osVersion) out.os_version = String(osVersion);
|
|
114
|
+
if (c.Model) out.device_model = String(c.Model);
|
|
115
|
+
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);
|
|
116
|
+
else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Bridge values, once the native module answers. Absent keys stay absent. */
|
|
121
|
+
export function nativeDeviceContext(info: {
|
|
122
|
+
platform?: string;
|
|
123
|
+
osVersion?: string;
|
|
124
|
+
deviceModel?: string;
|
|
125
|
+
} | null): Record<string, string> {
|
|
126
|
+
const out: Record<string, string> = {};
|
|
127
|
+
if (!info) return out;
|
|
128
|
+
if (info.osVersion) out.os_version = info.osVersion;
|
|
129
|
+
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Merge the integrator's context with what we detected — DETECTED WINS on the keys above.
|
|
135
|
+
*
|
|
136
|
+
* The inversion is deliberate. Config normally wins over defaults, but these are not defaults: they
|
|
137
|
+
* are measurements, and a measurement must not be overridable by a guess. Everything outside
|
|
138
|
+
* {@link DETECTED_CONTEXT_KEYS} — `language`, `timezone`, `network_type`, anything app-specific —
|
|
139
|
+
* is still the integrator's to set, untouched.
|
|
140
|
+
*/
|
|
141
|
+
export function mergeDeviceContext(
|
|
142
|
+
configured: Record<string, unknown> | undefined,
|
|
143
|
+
detected: Record<string, string> = staticDeviceContext(),
|
|
144
|
+
): Record<string, unknown> {
|
|
145
|
+
return { ...(configured ?? {}), ...detected };
|
|
146
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { ReplayConfig } from '../features/replay/public/types';
|
|
|
10
10
|
import type { DebugTransport } from './transport';
|
|
11
11
|
import type { SessionManager as SessionManagerType } from '../features/session/SessionManager';
|
|
12
12
|
import { logger } from '../core/logger/internalLogger';
|
|
13
|
+
import { resolvePlatformOS } from '../core/context/device';
|
|
13
14
|
|
|
14
15
|
// ─── State ──────────────────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -90,7 +91,7 @@ export function wireNativeCaptureDeferred(
|
|
|
90
91
|
byteSize: result.sizeBytes || Math.round(result.base64.length * 0.75),
|
|
91
92
|
format: (result.format ?? 'jpeg') as string,
|
|
92
93
|
frameHash: (result as any).frameHash as string | undefined,
|
|
93
|
-
platform: (
|
|
94
|
+
platform: resolvePlatformOS() as 'android' | 'ios',
|
|
94
95
|
};
|
|
95
96
|
} catch {
|
|
96
97
|
return null;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-BUG (React Native) — bug-report feature.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the web SDK's `features/bugreport/BugReportFeature.ts`, deliberately keeping its
|
|
5
|
+
* split: this class is THIN. It raises the capture signal and holds a `submitFn` injected by the
|
|
6
|
+
* facade, which owns session + transport and therefore owns the HTTP. Duplicating the submit here
|
|
7
|
+
* would give the two SDKs two contracts to drift apart.
|
|
8
|
+
*
|
|
9
|
+
* WHAT DOES NOT PORT
|
|
10
|
+
* The web feature lazy-imports a DOM launcher widget (`widget.ts`, 185 lines of document/CSS). RN
|
|
11
|
+
* has no DOM: the form is rendered by `EngagePromptView`, which already exists, already handles the
|
|
12
|
+
* native modal/keyboard behaviour, and is already what `shake_device` puts on screen. Building a
|
|
13
|
+
* second prompt system for this would be the same mistake in a different repo.
|
|
14
|
+
*
|
|
15
|
+
* TRIGGERING
|
|
16
|
+
* `shake_device` is already wired end to end — native CoreMotion/SensorManager →
|
|
17
|
+
* `engageShakeBridge` → `gestureTriggerDetector.reportShake()` → the `$gesture` signal lane → the
|
|
18
|
+
* engage trigger engine. This feature is a submit target for that flow, not a second detector.
|
|
19
|
+
*/
|
|
20
|
+
import { logger } from '../../core/logger/internalLogger';
|
|
21
|
+
import type { EngageAttachmentDraft } from '../engage/engageTypes';
|
|
22
|
+
|
|
23
|
+
/** Why a submit did not produce a report id. Mirrors the web SDK's discriminated failure union. */
|
|
24
|
+
export type BugFailure = 'init' | 'consent' | 'send' | 'empty';
|
|
25
|
+
|
|
26
|
+
/** Resolves to the report id, or to `{ e }` naming WHICH branch failed. */
|
|
27
|
+
export type BugSubmitResult = string | { e: BugFailure };
|
|
28
|
+
|
|
29
|
+
export interface BugReportInput {
|
|
30
|
+
/** The issue text. Required — a report with no description is not a report. */
|
|
31
|
+
message: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Reporter's email. Optional on purpose: the session usually already resolves to an end user, so
|
|
35
|
+
* the form should PREFILL this rather than demand it from someone who just shook their phone in
|
|
36
|
+
* frustration. `BugReport.reporterEmail` is nullable server-side.
|
|
37
|
+
*/
|
|
38
|
+
email?: string;
|
|
39
|
+
/**
|
|
40
|
+
* An approved voice note, produced by `features/engage/captureVoiceAttachment`.
|
|
41
|
+
*
|
|
42
|
+
* That module returns a transcript as a PREFILL and only marks `transcriptApproved` once the user
|
|
43
|
+
* has explicitly accepted the wording. This feature must never submit an unapproved draft — a
|
|
44
|
+
* machine transcription of someone's voice, sent without them confirming it, is a different
|
|
45
|
+
* product decision than the one that pipeline was built for.
|
|
46
|
+
*/
|
|
47
|
+
voice?: EngageAttachmentDraft;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type SubmitFn = (input: BugReportInput) => Promise<BugSubmitResult>;
|
|
51
|
+
|
|
52
|
+
export class BugReportFeature {
|
|
53
|
+
readonly name = 'bugReport';
|
|
54
|
+
|
|
55
|
+
private active = false;
|
|
56
|
+
private submitFn: SubmitFn | null = null;
|
|
57
|
+
/** Raises the 'report' capture signal so replay flushes its buffer into the current session. */
|
|
58
|
+
private signalFn: (() => void) | null = null;
|
|
59
|
+
|
|
60
|
+
get isActive(): boolean {
|
|
61
|
+
return this.active;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setup(hooks: { signal?: () => void } = {}): void {
|
|
65
|
+
this.signalFn = hooks.signal ?? null;
|
|
66
|
+
this.active = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Flush replay into the session so the report has its visual context.
|
|
71
|
+
*
|
|
72
|
+
* Fail-soft: replay may be disabled, paused, or unsupported on this build. A report without a
|
|
73
|
+
* replay anchor is still a useful report, so this must never block a submit.
|
|
74
|
+
*/
|
|
75
|
+
signalReport(): void {
|
|
76
|
+
try {
|
|
77
|
+
this.signalFn?.();
|
|
78
|
+
} catch {
|
|
79
|
+
/* never let a capture-signal failure break a bug report */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Wired by the facade after init — the full submit path (transport + session + context). */
|
|
84
|
+
setSubmit(fn: SubmitFn): void {
|
|
85
|
+
this.submitFn = fn;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Submit a report.
|
|
90
|
+
*
|
|
91
|
+
* Returns rather than throws, always. This is called from a UI the host app renders, often from a
|
|
92
|
+
* user gesture; an exception escaping into a React Native render tree is a redbox for the
|
|
93
|
+
* customer's user, over a bug report they were trying to send us.
|
|
94
|
+
*/
|
|
95
|
+
async submit(input: BugReportInput): Promise<BugSubmitResult> {
|
|
96
|
+
if (!input || !input.message || input.message.trim().length === 0) {
|
|
97
|
+
logger.warn('[scalebun] reportBug: a message is required');
|
|
98
|
+
return { e: 'empty' };
|
|
99
|
+
}
|
|
100
|
+
// No submitFn means the facade never finished wiring. That is 'init', not a send failure —
|
|
101
|
+
// the distinction is what tells an integrator to check their setup rather than their network.
|
|
102
|
+
if (!this.submitFn) return { e: 'init' };
|
|
103
|
+
|
|
104
|
+
if (input.voice && !input.voice.transcriptApproved) {
|
|
105
|
+
// Refused rather than silently stripped: dropping the attachment would send a report the user
|
|
106
|
+
// believes contains their voice note, and they would never learn it did not.
|
|
107
|
+
logger.warn('[scalebun] reportBug: voice note has no approved transcript — not sending');
|
|
108
|
+
return { e: 'empty' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// Order matters and mirrors the web path: signal BEFORE submit so the replay bytes are already
|
|
113
|
+
// in flight, and so the report's replay coordinate refers to a frame that exists. A flush
|
|
114
|
+
// cannot retroactively manufacture a frame, so a missing anchor must stay missing rather than
|
|
115
|
+
// become a misleading "start of session" link in the dashboard.
|
|
116
|
+
this.signalReport();
|
|
117
|
+
return await this.submitFn(input);
|
|
118
|
+
} catch {
|
|
119
|
+
return { e: 'send' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
teardown(): void {
|
|
124
|
+
this.submitFn = null;
|
|
125
|
+
this.signalFn = null;
|
|
126
|
+
this.active = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the body for POST /ingestion/sessions/:sessionId/bug-reports.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a PURE function with no transport, no bridge and no clock of its own, because the wire
|
|
5
|
+
* shape is a cross-repo contract and the only way to keep it honest is to be able to assert it
|
|
6
|
+
* directly. Every bound below is copied from the server DTO
|
|
7
|
+
* (wzb-dashboard-backend/src/features/bug-reports/dto/submit-bug-report.dto.ts) rather than guessed:
|
|
8
|
+
*
|
|
9
|
+
* clientReportId? string idempotency key -> BugReport.clientReportId (@unique)
|
|
10
|
+
* title? string <= 300
|
|
11
|
+
* message string <= 20000 -> BugReport.description (REQUIRED)
|
|
12
|
+
* email? string <= 320
|
|
13
|
+
* url? string <= 2000
|
|
14
|
+
* console? unknown[] recent breadcrumbs
|
|
15
|
+
* screenshotKey? string <= 512, [A-Za-z0-9._/-] and no ".."
|
|
16
|
+
* timestamp? number 2000-01-01 .. 2100-01-01 in epoch MILLIS
|
|
17
|
+
* replaySeq? int 0 .. 2147483647
|
|
18
|
+
* replaySegmentId? string <= 256
|
|
19
|
+
*
|
|
20
|
+
* Truncating here rather than letting the server 400 is deliberate: a 20,001-character description
|
|
21
|
+
* is a real report from a real annoyed user, and losing it to a validation error helps nobody. The
|
|
22
|
+
* ceiling is theirs, so we respect it at the edge. Nothing is silently REWRITTEN — only cut, and
|
|
23
|
+
* only at bounds the server would otherwise reject outright.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Mirrors SubmitBugReportDto. Optional keys are omitted entirely rather than sent as undefined. */
|
|
27
|
+
export interface BugReportPayload {
|
|
28
|
+
clientReportId?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
console?: unknown[];
|
|
34
|
+
screenshotKey?: string;
|
|
35
|
+
timestamp?: number;
|
|
36
|
+
replaySeq?: number;
|
|
37
|
+
replaySegmentId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReplayCoord {
|
|
41
|
+
seq?: number | null;
|
|
42
|
+
segmentId?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MAX_TITLE = 300;
|
|
46
|
+
const MAX_MESSAGE = 20_000;
|
|
47
|
+
const MAX_EMAIL = 320;
|
|
48
|
+
const MAX_SEGMENT_ID = 256;
|
|
49
|
+
/** The server rejects outside these; a drifted device clock must not cost us the whole report. */
|
|
50
|
+
const TS_MIN = 946684800000; // 2000-01-01
|
|
51
|
+
const TS_MAX = 4102444800000; // 2100-01-01
|
|
52
|
+
const MAX_BREADCRUMBS = 50;
|
|
53
|
+
|
|
54
|
+
function clip(v: string | undefined | null, max: number): string | undefined {
|
|
55
|
+
if (typeof v !== 'string') return undefined;
|
|
56
|
+
const t = v.trim();
|
|
57
|
+
return t.length === 0 ? undefined : t.slice(0, max);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildBugReportPayload(
|
|
61
|
+
input: { message: string; title?: string; email?: string; clientReportId?: string },
|
|
62
|
+
breadcrumbs: readonly unknown[] = [],
|
|
63
|
+
ctx: { replayCoord?: ReplayCoord | null; timestamp?: number } = {},
|
|
64
|
+
): BugReportPayload {
|
|
65
|
+
const message = (input.message ?? '').trim().slice(0, MAX_MESSAGE);
|
|
66
|
+
|
|
67
|
+
const payload: BugReportPayload = { message };
|
|
68
|
+
|
|
69
|
+
const title = clip(input.title, MAX_TITLE);
|
|
70
|
+
if (title) payload.title = title;
|
|
71
|
+
|
|
72
|
+
const email = clip(input.email, MAX_EMAIL);
|
|
73
|
+
if (email) payload.email = email;
|
|
74
|
+
|
|
75
|
+
const clientReportId = clip(input.clientReportId, 200);
|
|
76
|
+
if (clientReportId) payload.clientReportId = clientReportId;
|
|
77
|
+
|
|
78
|
+
if (breadcrumbs.length > 0) payload.console = breadcrumbs.slice(-MAX_BREADCRUMBS);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* An out-of-range clock is DROPPED, not clamped.
|
|
82
|
+
*
|
|
83
|
+
* Clamping would invent a `reportedAt` the device never observed, and the server keeps that column
|
|
84
|
+
* separate from `createdAt` precisely so the two can be compared. A missing client time is honest;
|
|
85
|
+
* a fabricated one silently corrupts that comparison.
|
|
86
|
+
*/
|
|
87
|
+
const ts = ctx.timestamp;
|
|
88
|
+
if (typeof ts === 'number' && Number.isFinite(ts) && ts >= TS_MIN && ts <= TS_MAX) {
|
|
89
|
+
payload.timestamp = ts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The replay anchor is included ONLY when a frame actually existed at submit time.
|
|
94
|
+
*
|
|
95
|
+
* Defaulting `replaySeq` to 0 would point every anchor-less report at the start of the session,
|
|
96
|
+
* which reads in the dashboard exactly like a real anchor and is wrong. Replay may be disabled,
|
|
97
|
+
* paused, or yet to produce a frame; absent must stay absent.
|
|
98
|
+
*/
|
|
99
|
+
const seq = ctx.replayCoord?.seq;
|
|
100
|
+
if (typeof seq === 'number' && Number.isInteger(seq) && seq >= 0 && seq <= 2147483647) {
|
|
101
|
+
payload.replaySeq = seq;
|
|
102
|
+
const segmentId = clip(ctx.replayCoord?.segmentId ?? undefined, MAX_SEGMENT_ID);
|
|
103
|
+
if (segmentId) payload.replaySegmentId = segmentId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `url` is intentionally NEVER set on React Native.
|
|
108
|
+
*
|
|
109
|
+
* The field means a page URL. The nearest mobile concept is the current screen name, and putting
|
|
110
|
+
* a screen name in a column called `url` is exactly the field-misuse that makes a dashboard show
|
|
111
|
+
* one thing and mean another. Screen context already reaches the report through the session it is
|
|
112
|
+
* attached to. If mobile needs its own field, that is a schema change, not a reused column.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
@@ -18,7 +18,7 @@ import { noThrow } from '../../core/lifecycle/crashSafe';
|
|
|
18
18
|
import { SessionManager } from '../session/SessionManager';
|
|
19
19
|
|
|
20
20
|
/** Where the crash originated. */
|
|
21
|
-
export type CrashSource = 'js' | 'handled' | 'native';
|
|
21
|
+
export type CrashSource = 'js' | 'handled' | 'native' | 'manual';
|
|
22
22
|
|
|
23
23
|
/** Normalized, transport-agnostic crash report. */
|
|
24
24
|
export interface CrashReport {
|
|
@@ -40,6 +40,25 @@ export interface CrashReport {
|
|
|
40
40
|
timestamp?: string;
|
|
41
41
|
/** Free-form extra context (thread name, signal, cause, ...). */
|
|
42
42
|
metadata?: Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* The session this crash BELONGS to, when that is not the current one.
|
|
45
|
+
*
|
|
46
|
+
* Set only by a recovered native crash: it happened in a process that is already gone, so the
|
|
47
|
+
* session it belongs to is not the session running now. Without it the report is routed through
|
|
48
|
+
* whichever SessionManager exists at drain time and lands on the NEW session — marking a healthy
|
|
49
|
+
* session crashed and the crashed one clean.
|
|
50
|
+
*
|
|
51
|
+
* Absent means "the current session", which is correct for every live report.
|
|
52
|
+
*/
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Stable id for a recovered native crash — the idempotency key.
|
|
56
|
+
*
|
|
57
|
+
* Generated ONCE when the crash was first written to disk, never per attempt, so any number of
|
|
58
|
+
* retries collapse to one backend record. An id minted at upload time would defeat dedupe
|
|
59
|
+
* entirely and turn every retry into a fresh duplicate crash.
|
|
60
|
+
*/
|
|
61
|
+
crashId?: string;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
@@ -111,11 +130,31 @@ export class BackendErrorsSink implements CrashSink {
|
|
|
111
130
|
deliver(report: CrashReport): void {
|
|
112
131
|
const transport = SessionManager.getExistingInstance()?.getBackendTransport();
|
|
113
132
|
if (!transport) return;
|
|
133
|
+
// D12: this used to forward four fields and drop the rest — so a native
|
|
134
|
+
// crash's signal/thread/exception-type were captured on device and
|
|
135
|
+
// discarded one call before the wire, every RN error row had a NULL
|
|
136
|
+
// platform (making Issue.primaryPlatform mean "web-only"), and fatality
|
|
137
|
+
// was indistinguishable from a handled error. The receiver has always
|
|
138
|
+
// accepted all of these.
|
|
114
139
|
transport.queueError({
|
|
115
140
|
type: report.type,
|
|
116
141
|
message: report.message,
|
|
117
142
|
stack: report.stack,
|
|
118
143
|
screenName: report.screenName,
|
|
144
|
+
platform: report.platform,
|
|
145
|
+
isFatal: report.isFatal,
|
|
146
|
+
metadata: report.metadata,
|
|
147
|
+
/**
|
|
148
|
+
* Carried through so the transport can address the ORIGINAL session.
|
|
149
|
+
*
|
|
150
|
+
* Set only on a recovered native crash, whose session died with its process. Omitted
|
|
151
|
+
* for every live report, where the current session is the right answer — so this
|
|
152
|
+
* changes nothing about normal error delivery.
|
|
153
|
+
*/
|
|
154
|
+
...(report.sessionId ? { sessionId: report.sessionId } : {}),
|
|
155
|
+
// Becomes SessionError.clientId, which is @unique — that constraint is what turns a
|
|
156
|
+
// retried crash into one record instead of a duplicate.
|
|
157
|
+
...(report.crashId ? { clientId: report.crashId } : {}),
|
|
119
158
|
});
|
|
120
159
|
// Best-effort immediate delivery so the crash surfaces quickly.
|
|
121
160
|
transport.flushErrors().catch(() => {});
|
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
import { createEnvelope } from '../../pipeline/envelope/envelope';
|
|
2
|
-
import { memoryQueue } from '../../pipeline/queue/memoryQueue';
|
|
3
1
|
import { noThrow } from '../../core/lifecycle/crashSafe';
|
|
2
|
+
import { getCrashReporter } from './CrashReporter';
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Manual error/message capture — the `Sentry.captureException` equivalent.
|
|
6
|
+
*
|
|
7
|
+
* D3: these used to write envelopes into `memoryQueue`, a queue whose only
|
|
8
|
+
* drain (`Dispatcher.flush`) was registered in the DI container and never
|
|
9
|
+
* resolved by anything — every capture was accumulated and discarded. They
|
|
10
|
+
* were also never exported, so the two entry points were reachable by nobody
|
|
11
|
+
* and delivered to nowhere.
|
|
12
|
+
*
|
|
13
|
+
* Now they ride the SAME pipeline as automatic crash capture (CrashReporter →
|
|
14
|
+
* BackendErrorsSink → `/ingestion/sessions/:id/errors`) — the one path in this
|
|
15
|
+
* SDK that is proven to land rows.
|
|
16
|
+
*/
|
|
17
|
+
export function captureException(
|
|
18
|
+
error: Error,
|
|
19
|
+
extra?: { screenName?: string; metadata?: Record<string, unknown> },
|
|
20
|
+
): void {
|
|
6
21
|
noThrow(() => {
|
|
7
|
-
|
|
22
|
+
getCrashReporter().report({
|
|
23
|
+
type: 'handled',
|
|
8
24
|
message: error.message,
|
|
9
25
|
stack: error.stack,
|
|
10
|
-
|
|
26
|
+
isFatal: false,
|
|
27
|
+
source: 'manual',
|
|
28
|
+
screenName: extra?.screenName,
|
|
29
|
+
metadata: extra?.metadata,
|
|
11
30
|
});
|
|
12
|
-
memoryQueue.add(envelope);
|
|
13
31
|
});
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
export function captureMessage(
|
|
34
|
+
export function captureMessage(
|
|
35
|
+
message: string,
|
|
36
|
+
extra?: { screenName?: string; metadata?: Record<string, unknown> },
|
|
37
|
+
): void {
|
|
17
38
|
noThrow(() => {
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
getCrashReporter().report({
|
|
40
|
+
type: 'handled',
|
|
41
|
+
message,
|
|
42
|
+
isFatal: false,
|
|
43
|
+
source: 'manual',
|
|
44
|
+
screenName: extra?.screenName,
|
|
45
|
+
metadata: extra?.metadata,
|
|
46
|
+
});
|
|
20
47
|
});
|
|
21
48
|
}
|