@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
|
@@ -13,14 +13,128 @@ internal object CrashStorage {
|
|
|
13
13
|
|
|
14
14
|
private const val FILE_NAME = "scalebun_native_crash.json"
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The session that is live RIGHT NOW, written once per session and read by the crash handler.
|
|
18
|
+
*
|
|
19
|
+
* WHY A SEPARATE FILE, AND WHY WRITTEN AHEAD OF TIME.
|
|
20
|
+
*
|
|
21
|
+
* A crash record survives the process; the JS session id did not, because it only ever existed
|
|
22
|
+
* in JS memory. So on the next launch there was nothing to attribute the crash to except
|
|
23
|
+
* whichever session had just been created — the crash from session A landed on session B.
|
|
24
|
+
*
|
|
25
|
+
* The fix is not to hand the id to the crash handler (a fatal handler cannot call into JS, and
|
|
26
|
+
* must not allocate or block), but to make sure it is ALREADY ON DISK before any crash can
|
|
27
|
+
* happen. This file is that guarantee: written once when a session starts, read synchronously
|
|
28
|
+
* inside `uncaughtException`.
|
|
29
|
+
*
|
|
30
|
+
* Kept separate from the crash record so a stale id can never resurrect a cleared crash, and so
|
|
31
|
+
* clearing a drained crash does not disturb the live session's identity.
|
|
32
|
+
*/
|
|
33
|
+
private const val SESSION_FILE_NAME = "scalebun_active_session.txt"
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Record the session that is now live. Called once per session from JS, never per event.
|
|
37
|
+
*
|
|
38
|
+
* Synchronous by design: it must be durable before the next line of app code runs, because the
|
|
39
|
+
* next line of app code is exactly what might crash. One small write per session is affordable;
|
|
40
|
+
* the same write per event would not be.
|
|
41
|
+
*/
|
|
42
|
+
fun setActiveSessionId(context: Context, sessionId: String?) {
|
|
43
|
+
try {
|
|
44
|
+
val file = File(context.filesDir, SESSION_FILE_NAME)
|
|
45
|
+
if (sessionId.isNullOrEmpty()) file.delete() else file.writeText(sessionId)
|
|
46
|
+
} catch (_: Exception) {
|
|
47
|
+
// A missing session id degrades attribution; it must never break the app.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The live session id, or null.
|
|
53
|
+
*
|
|
54
|
+
* Called from the uncaught-exception handler, so it does exactly one thing: read a small file.
|
|
55
|
+
* No allocation beyond the string, no locks, no JS, no network.
|
|
56
|
+
*/
|
|
57
|
+
fun activeSessionId(context: Context): String? {
|
|
58
|
+
return try {
|
|
59
|
+
val file = File(context.filesDir, SESSION_FILE_NAME)
|
|
60
|
+
if (!file.exists()) null else file.readText().trim().ifEmpty { null }
|
|
61
|
+
} catch (_: Exception) {
|
|
62
|
+
null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How many undelivered crashes we keep.
|
|
68
|
+
*
|
|
69
|
+
* One process produces one fatal crash, so this is not about a burst — it is about a device
|
|
70
|
+
* that crashes, restarts offline, and crashes again before the first crash could be uploaded.
|
|
71
|
+
* With a single fixed filename the second crash silently overwrote the first.
|
|
72
|
+
*
|
|
73
|
+
* Three is deliberately small: crashes are only pending while delivery is failing, and an
|
|
74
|
+
* unbounded spool on a device that crashes in a loop is its own bug. When full, the OLDEST is
|
|
75
|
+
* dropped — a fresh crash is better evidence than a stale one, and the alternative (dropping the
|
|
76
|
+
* new one) would hide an ongoing crash loop behind the first crash of the day.
|
|
77
|
+
*/
|
|
78
|
+
private const val MAX_PENDING = 3
|
|
79
|
+
private const val PREFIX = "scalebun_native_crash"
|
|
80
|
+
|
|
81
|
+
/** Every pending crash file, oldest first. */
|
|
82
|
+
private fun pendingFiles(context: Context): List<File> =
|
|
83
|
+
(context.filesDir.listFiles { f -> f.name.startsWith(PREFIX) && f.name.endsWith(".json") }
|
|
84
|
+
?: emptyArray())
|
|
85
|
+
.sortedBy { it.lastModified() }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Persist a crash. Each gets its own file and its own STABLE id.
|
|
89
|
+
*
|
|
90
|
+
* The id is generated HERE, once, when the crash is first written — never at upload time. It is
|
|
91
|
+
* what makes retry safe: the backend dedupes on it, so the same crash delivered any number of
|
|
92
|
+
* times produces one record. An id minted per attempt would defeat that completely.
|
|
93
|
+
*/
|
|
16
94
|
fun write(context: Context, crashData: JSONObject) {
|
|
17
95
|
try {
|
|
18
|
-
|
|
96
|
+
val crashId = crashData.optString("crashId", "").ifEmpty {
|
|
97
|
+
// Time + identity hash: unique in practice, and derivable without allocation-heavy
|
|
98
|
+
// APIs so the same scheme can be mirrored on the signal path.
|
|
99
|
+
"crash-${System.currentTimeMillis()}-${android.os.Process.myPid()}"
|
|
100
|
+
}
|
|
101
|
+
crashData.put("crashId", crashId)
|
|
102
|
+
|
|
103
|
+
val existing = pendingFiles(context)
|
|
104
|
+
if (existing.size >= MAX_PENDING) {
|
|
105
|
+
existing.take(existing.size - MAX_PENDING + 1).forEach { runCatching { it.delete() } }
|
|
106
|
+
}
|
|
107
|
+
File(context.filesDir, "$PREFIX-$crashId.json").writeText(crashData.toString())
|
|
19
108
|
} catch (_: Exception) {
|
|
20
109
|
// Must not throw — called from uncaught exception handler
|
|
21
110
|
}
|
|
22
111
|
}
|
|
23
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Every pending crash, WITHOUT deleting anything.
|
|
115
|
+
*
|
|
116
|
+
* Read used to clear as a side effect, so a failed upload lost the crash permanently: the only
|
|
117
|
+
* durable copy was destroyed before anyone knew the network had worked. Deletion is now a
|
|
118
|
+
* separate, explicit act performed after the backend accepts — see [clearCrash].
|
|
119
|
+
*/
|
|
120
|
+
fun readAll(context: Context): List<JSONObject> =
|
|
121
|
+
pendingFiles(context).mapNotNull { f ->
|
|
122
|
+
runCatching { JSONObject(f.readText()) }.getOrNull()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Drop one delivered crash by its stable id. Safe to call for an id that is already gone. */
|
|
126
|
+
fun clearCrash(context: Context, crashId: String) {
|
|
127
|
+
try {
|
|
128
|
+
File(context.filesDir, "$PREFIX-$crashId.json").delete()
|
|
129
|
+
// Legacy single-file record predating per-crash ids.
|
|
130
|
+
if (crashId.isEmpty()) File(context.filesDir, FILE_NAME).delete()
|
|
131
|
+
} catch (_: Exception) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The LEGACY single-file record, kept only so a crash written by a previous SDK version is not
|
|
136
|
+
* stranded on disk after an app update. Read non-destructively like everything else now.
|
|
137
|
+
*/
|
|
24
138
|
fun read(context: Context): JSONObject? {
|
|
25
139
|
return try {
|
|
26
140
|
val file = File(context.filesDir, FILE_NAME)
|
|
@@ -49,6 +49,20 @@ class NativeCrashHandler private constructor(
|
|
|
49
49
|
put("timestamp", System.currentTimeMillis().toDouble())
|
|
50
50
|
put("platform", "android")
|
|
51
51
|
if (causeInfo != null) put("cause", causeInfo)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The session this crash actually belongs to.
|
|
55
|
+
*
|
|
56
|
+
* Read from disk, where it was written when the session started — NOT asked for
|
|
57
|
+
* now. A fatal handler cannot call into JS, and the JS thread may already be gone.
|
|
58
|
+
* Without this the crash arrives on the next launch with no identity, and gets
|
|
59
|
+
* attributed to whichever session exists by then: the crash from session A lands
|
|
60
|
+
* on session B, marking a healthy session crashed and a crashed one clean.
|
|
61
|
+
*
|
|
62
|
+
* Omitted rather than written empty when unknown (a crash before any session
|
|
63
|
+
* started), so the reader can tell "no session" from "session ''".
|
|
64
|
+
*/
|
|
65
|
+
CrashStorage.activeSessionId(context)?.let { put("sessionId", it) }
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
CrashStorage.write(context, data)
|
|
@@ -913,25 +913,41 @@ class ReplaySdkModule(reactContext: ReactApplicationContext) : ReplaySdkSpec(rea
|
|
|
913
913
|
@ReactMethod
|
|
914
914
|
override fun stopVoiceRecording(promise: Promise) {
|
|
915
915
|
try {
|
|
916
|
-
|
|
916
|
+
// Order matters twice here:
|
|
917
|
+
// 1. The recorder stops FIRST — it owns the mic while running, and on
|
|
918
|
+
// most devices a concurrent capture silences the recognition engine
|
|
919
|
+
// (the transcript arrives empty). Releasing the mic before asking
|
|
920
|
+
// the engine to finalize gives its last buffer a chance to flush.
|
|
921
|
+
// 2. The transcript is read via the ASYNC stop: stopListening() →
|
|
922
|
+
// onResults is not synchronous, and the old sync read raced it —
|
|
923
|
+
// returning partials or nothing even when the engine had a result.
|
|
917
924
|
val clip = voiceRecorder?.stop()
|
|
925
|
+
val transcriber = voiceTranscriber
|
|
918
926
|
voiceRecorder = null
|
|
919
927
|
voiceTranscriber = null
|
|
920
928
|
if (clip == null) {
|
|
929
|
+
transcriber?.stop()
|
|
921
930
|
promise.resolve(null)
|
|
922
931
|
return
|
|
923
932
|
}
|
|
924
933
|
// Bytes are read into base64 by the recorder; drop the temp file now.
|
|
925
934
|
java.io.File(clip.filePath).delete()
|
|
926
|
-
val
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
935
|
+
val resolveWith = { transcript: com.scalebun.replaysdk.voice.VoiceTranscriber.TranscriptResult? ->
|
|
936
|
+
val result = Arguments.createMap().apply {
|
|
937
|
+
putString("base64", clip.base64)
|
|
938
|
+
putString("mime", "audio/mp4")
|
|
939
|
+
putInt("durationMs", clip.durationMs)
|
|
940
|
+
putInt("byteSize", clip.byteSize)
|
|
941
|
+
putString("transcript", transcript?.text ?: "")
|
|
942
|
+
putBoolean("transcriptAvailable", transcript?.available ?: false)
|
|
943
|
+
}
|
|
944
|
+
promise.resolve(result)
|
|
945
|
+
}
|
|
946
|
+
if (transcriber == null) {
|
|
947
|
+
resolveWith(null)
|
|
948
|
+
} else {
|
|
949
|
+
transcriber.stop { transcript -> resolveWith(transcript) }
|
|
933
950
|
}
|
|
934
|
-
promise.resolve(result)
|
|
935
951
|
} catch (e: Exception) {
|
|
936
952
|
voiceRecorder = null
|
|
937
953
|
voiceTranscriber = null
|
|
@@ -31,6 +31,11 @@ class VoiceTranscriber(private val context: Context) {
|
|
|
31
31
|
@Volatile private var transcript: String = ""
|
|
32
32
|
@Volatile private var available: Boolean = false
|
|
33
33
|
|
|
34
|
+
companion object {
|
|
35
|
+
/** How long stop() waits for the engine's final onResults before giving up. */
|
|
36
|
+
private const val FINAL_RESULT_GRACE_MS = 700L
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
/** Start a live on-device recognition session. Best-effort — never throws. */
|
|
35
40
|
fun start() {
|
|
36
41
|
main.post {
|
|
@@ -63,7 +68,13 @@ class VoiceTranscriber(private val context: Context) {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Stop the session and return the accumulated transcript (may be empty).
|
|
73
|
+
*
|
|
74
|
+
* Kept for binary compatibility; prefer [stop] with a callback — this sync
|
|
75
|
+
* form returns BEFORE the recognizer's final onResults can arrive, so it only
|
|
76
|
+
* ever sees partials.
|
|
77
|
+
*/
|
|
67
78
|
fun stop(): TranscriptResult {
|
|
68
79
|
main.post {
|
|
69
80
|
try {
|
|
@@ -76,12 +87,53 @@ class VoiceTranscriber(private val context: Context) {
|
|
|
76
87
|
return TranscriptResult(transcript.trim(), available)
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Stop the session and deliver the transcript AFTER the engine has had a
|
|
92
|
+
* chance to emit its final result. stopListening() → onResults is
|
|
93
|
+
* asynchronous; the sync stop() raced it and returned partials-only (or
|
|
94
|
+
* nothing). We wait up to [FINAL_RESULT_GRACE_MS], completing early when
|
|
95
|
+
* onResults lands.
|
|
96
|
+
*/
|
|
97
|
+
fun stop(onDone: (TranscriptResult) -> Unit) {
|
|
98
|
+
main.post {
|
|
99
|
+
val rec = recognizer
|
|
100
|
+
recognizer = null
|
|
101
|
+
if (rec == null || !available) {
|
|
102
|
+
onDone(TranscriptResult(transcript.trim(), available))
|
|
103
|
+
return@post
|
|
104
|
+
}
|
|
105
|
+
var completed = false
|
|
106
|
+
val finish = {
|
|
107
|
+
if (!completed) {
|
|
108
|
+
completed = true
|
|
109
|
+
try {
|
|
110
|
+
rec.destroy()
|
|
111
|
+
} catch (_: Exception) {
|
|
112
|
+
}
|
|
113
|
+
onDone(TranscriptResult(transcript.trim(), available))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
pendingFinish = finish
|
|
117
|
+
try {
|
|
118
|
+
rec.stopListening()
|
|
119
|
+
} catch (_: Exception) {
|
|
120
|
+
finish()
|
|
121
|
+
return@post
|
|
122
|
+
}
|
|
123
|
+
main.postDelayed({ finish() }, FINAL_RESULT_GRACE_MS)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Volatile private var pendingFinish: (() -> Unit)? = null
|
|
128
|
+
|
|
79
129
|
private val listener = object : RecognitionListener {
|
|
80
130
|
override fun onResults(results: Bundle?) {
|
|
81
131
|
results
|
|
82
132
|
?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
|
|
83
133
|
?.firstOrNull()
|
|
84
134
|
?.let { if (it.isNotBlank()) transcript = it }
|
|
135
|
+
// Final result is in — no reason to sit out the rest of the grace window.
|
|
136
|
+
main.post { pendingFinish?.invoke() }
|
|
85
137
|
}
|
|
86
138
|
|
|
87
139
|
override fun onPartialResults(partial: Bundle?) {
|
|
@@ -91,7 +143,21 @@ class VoiceTranscriber(private val context: Context) {
|
|
|
91
143
|
?.let { if (it.isNotBlank()) transcript = it }
|
|
92
144
|
}
|
|
93
145
|
|
|
94
|
-
override fun onError(error: Int) {
|
|
146
|
+
override fun onError(error: Int) {
|
|
147
|
+
// The errors this swallowed are exactly the ones that explain an empty
|
|
148
|
+
// transcript in the field: ERROR_AUDIO(3)/ERROR_RECOGNIZER_BUSY(8) mean
|
|
149
|
+
// the mic was contended (a concurrent MediaRecorder usually wins and the
|
|
150
|
+
// engine hears silence); ERROR_NO_MATCH(7) means it heard but understood
|
|
151
|
+
// nothing. Keep them visible.
|
|
152
|
+
android.util.Log.i("ScaleBunVoice", "SpeechRecognizer error=$error (transcript so far: ${transcript.length} chars)")
|
|
153
|
+
if (error == SpeechRecognizer.ERROR_AUDIO ||
|
|
154
|
+
error == SpeechRecognizer.ERROR_RECOGNIZER_BUSY ||
|
|
155
|
+
error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS
|
|
156
|
+
) {
|
|
157
|
+
available = false
|
|
158
|
+
}
|
|
159
|
+
main.post { pendingFinish?.invoke() }
|
|
160
|
+
}
|
|
95
161
|
override fun onReadyForSpeech(params: Bundle?) {}
|
|
96
162
|
override fun onBeginningOfSpeech() {}
|
|
97
163
|
override fun onRmsChanged(rmsdB: Float) {}
|
|
@@ -48,16 +48,62 @@ class ScaleBunCrashModule(reactContext: ReactApplicationContext) :
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Record which session is live, so a crash can name it.
|
|
53
|
+
*
|
|
54
|
+
* Called once when a session starts — never per event. The write is small and synchronous
|
|
55
|
+
* because it has to be durable before the next line of app code runs, and the next line of app
|
|
56
|
+
* code is exactly what might crash.
|
|
57
|
+
*/
|
|
58
|
+
@ReactMethod
|
|
59
|
+
override fun setActiveSessionId(sessionId: String?, promise: Promise) {
|
|
60
|
+
try {
|
|
61
|
+
CrashStorage.setActiveSessionId(reactApplicationContext.applicationContext, sessionId)
|
|
62
|
+
promise.resolve(true)
|
|
63
|
+
} catch (e: Exception) {
|
|
64
|
+
// Attribution is best-effort; never fail the caller over it.
|
|
65
|
+
promise.resolve(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delete one crash AFTER the backend has accepted it.
|
|
71
|
+
*
|
|
72
|
+
* Separating deletion from the read is the whole of at-least-once: the durable copy outlives
|
|
73
|
+
* every failed attempt and disappears only once the crash is known to be recorded.
|
|
74
|
+
*/
|
|
75
|
+
@ReactMethod
|
|
76
|
+
override fun clearPendingCrash(crashId: String?, promise: Promise) {
|
|
77
|
+
try {
|
|
78
|
+
CrashStorage.clearCrash(reactApplicationContext.applicationContext, crashId ?: "")
|
|
79
|
+
promise.resolve(true)
|
|
80
|
+
} catch (e: Exception) {
|
|
81
|
+
// Failing to delete is harmless — the crash is already recorded, and the backend dedupes
|
|
82
|
+
// on crashId, so a redelivery produces no second row.
|
|
83
|
+
promise.resolve(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
@ReactMethod
|
|
52
88
|
override fun drainPendingCrash(promise: Promise) {
|
|
53
89
|
try {
|
|
54
90
|
val context = reactApplicationContext.applicationContext
|
|
55
|
-
|
|
91
|
+
/**
|
|
92
|
+
* NON-DESTRUCTIVE.
|
|
93
|
+
*
|
|
94
|
+
* This used to clear the record the moment it was read, so a failed upload destroyed the
|
|
95
|
+
* only durable copy and the crash was gone forever — the app had proof of a crash,
|
|
96
|
+
* deleted it, and then failed to deliver it. Deletion is now `clearPendingCrash`, called
|
|
97
|
+
* by JS only after the backend has accepted.
|
|
98
|
+
*
|
|
99
|
+
* Oldest first: if several are pending, the earliest crash is the one that has been
|
|
100
|
+
* waiting longest and is most at risk of being aged out.
|
|
101
|
+
*/
|
|
102
|
+
val crash = CrashStorage.readAll(context).firstOrNull() ?: CrashStorage.read(context)
|
|
56
103
|
if (crash == null) {
|
|
57
104
|
promise.resolve(null)
|
|
58
105
|
return
|
|
59
106
|
}
|
|
60
|
-
CrashStorage.clear(context)
|
|
61
107
|
|
|
62
108
|
val result = Arguments.createMap()
|
|
63
109
|
result.putString("name", crash.optString("name", "crash.native"))
|
|
@@ -67,6 +113,24 @@ class ScaleBunCrashModule(reactContext: ReactApplicationContext) :
|
|
|
67
113
|
result.putDouble("timestamp", crash.optDouble("timestamp"))
|
|
68
114
|
result.putString("platform", "android")
|
|
69
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The session the crash belonged to — the whole point of the fix, and this mapper
|
|
118
|
+
* ENUMERATES its fields, so a value not named here is silently dropped no matter how
|
|
119
|
+
* carefully it was written on the crash path.
|
|
120
|
+
*
|
|
121
|
+
* Absent on records written before this shipped, and on a crash that happened before
|
|
122
|
+
* any session started. Left unset in both cases rather than defaulted, so the JS side
|
|
123
|
+
* can tell "unknown" from a real id and fall back deliberately.
|
|
124
|
+
*/
|
|
125
|
+
val sid = crash.optString("sessionId", "")
|
|
126
|
+
if (sid.isNotEmpty()) result.putString("sessionId", sid)
|
|
127
|
+
|
|
128
|
+
// The STABLE id. It is what the backend dedupes on, and what JS hands back to
|
|
129
|
+
// clearPendingCrash once delivery succeeds — so it has to survive this enumerating
|
|
130
|
+
// mapper as well.
|
|
131
|
+
val cid = crash.optString("crashId", "")
|
|
132
|
+
if (cid.isNotEmpty()) result.putString("crashId", cid)
|
|
133
|
+
|
|
70
134
|
val stackJson = crash.optJSONArray("stackTrace")
|
|
71
135
|
if (stackJson != null) {
|
|
72
136
|
val frames = Arguments.createArray()
|
|
@@ -30,6 +30,15 @@ class SlotManager(private val filesDir: File) {
|
|
|
30
30
|
private const val BUNDLE_FILENAME = "bundle.jsbundle"
|
|
31
31
|
private const val META_FILENAME = "meta.json"
|
|
32
32
|
private const val BOOT_MARKER_FILENAME = "boot_marker"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Launches a freshly-applied bundle gets to call markHealthy() before the
|
|
36
|
+
* guard treats it as crash-looping. 2 = one retry: a single unlucky boot
|
|
37
|
+
* (OOM kill, user swipe-away during startup) does not cost the update,
|
|
38
|
+
* but a bundle that genuinely cannot start is gone by the second launch.
|
|
39
|
+
*/
|
|
40
|
+
private const val MAX_BOOT_ATTEMPTS = 2
|
|
41
|
+
private const val REVERT_RECORD_FILENAME = "last_revert.json"
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
private val otaRoot: File get() = File(filesDir, OTA_DIR)
|
|
@@ -37,6 +46,7 @@ class SlotManager(private val filesDir: File) {
|
|
|
37
46
|
private val nextSlot: File get() = File(otaRoot, "next")
|
|
38
47
|
private val previousSlot: File get() = File(otaRoot, "previous")
|
|
39
48
|
private val bootMarker: File get() = File(otaRoot, BOOT_MARKER_FILENAME)
|
|
49
|
+
private val revertRecord: File get() = File(otaRoot, REVERT_RECORD_FILENAME)
|
|
40
50
|
|
|
41
51
|
/**
|
|
42
52
|
* Returns the absolute path to the active JS bundle, or null if no OTA
|
|
@@ -157,9 +167,26 @@ class SlotManager(private val filesDir: File) {
|
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
169
|
* Revert to previous slot (boot-guard triggered or manual rollback).
|
|
170
|
+
*
|
|
171
|
+
* `reason` is persisted so JS can report WHY the device is no longer on the
|
|
172
|
+
* bundle it installed. Without it an auto-revert was invisible to the
|
|
173
|
+
* dashboard: `bootGuardReverted` was read by the orchestrator and written by
|
|
174
|
+
* nobody, so every crash-guard rollback recorded zero rollbacks.
|
|
160
175
|
*/
|
|
161
|
-
|
|
176
|
+
@JvmOverloads
|
|
177
|
+
fun revert(reason: String? = null): Boolean {
|
|
162
178
|
try {
|
|
179
|
+
if (reason != null) {
|
|
180
|
+
runCatching {
|
|
181
|
+
otaRoot.mkdirs()
|
|
182
|
+
revertRecord.writeText(
|
|
183
|
+
JSONObject()
|
|
184
|
+
.put("reason", reason)
|
|
185
|
+
.put("at", System.currentTimeMillis())
|
|
186
|
+
.toString(),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
163
190
|
if (!previousSlot.exists()) {
|
|
164
191
|
Log.w(TAG, "No previous/ to revert to — clearing current/")
|
|
165
192
|
currentSlot.deleteRecursively()
|
|
@@ -228,12 +255,39 @@ class SlotManager(private val filesDir: File) {
|
|
|
228
255
|
"Boot guard cannot verify this bundle — marker or meta has no sha256. " +
|
|
229
256
|
"Leaving it in place; restage with a current SDK to re-enable the check.",
|
|
230
257
|
)
|
|
231
|
-
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
if (currentId != markerId) {
|
|
232
261
|
Log.w(TAG, "Boot marker sha256 mismatch — auto-reverting")
|
|
233
262
|
revert()
|
|
263
|
+
return
|
|
234
264
|
}
|
|
235
|
-
|
|
236
|
-
//
|
|
265
|
+
|
|
266
|
+
// ── Crash-loop revert ────────────────────────────────────────────
|
|
267
|
+
// Matching hashes mean the bundle is INTACT, which says nothing
|
|
268
|
+
// about whether it RUNS. A bundle that throws at JS init installs
|
|
269
|
+
// cleanly, matches every boot, and never reaches markHealthy() — so
|
|
270
|
+
// a hash-only guard let it crash-loop forever, bricking the app
|
|
271
|
+
// until reinstall. That is the exact failure an OTA boot guard
|
|
272
|
+
// exists to prevent.
|
|
273
|
+
//
|
|
274
|
+
// The marker is therefore also a boot-attempt counter: every launch
|
|
275
|
+
// that starts with the marker still present increments it. Reaching
|
|
276
|
+
// the limit means the bundle has been given MAX_BOOT_ATTEMPTS
|
|
277
|
+
// chances to call markHealthy() and never did → revert.
|
|
278
|
+
val attempts = markerJson.optInt("bootAttempts", 0) + 1
|
|
279
|
+
if (attempts >= MAX_BOOT_ATTEMPTS) {
|
|
280
|
+
Log.w(
|
|
281
|
+
TAG,
|
|
282
|
+
"Bundle failed to become healthy in $attempts launch(es) — auto-reverting",
|
|
283
|
+
)
|
|
284
|
+
revert(reason = "boot_crash_guard")
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
markerJson.put("bootAttempts", attempts)
|
|
288
|
+
bootMarker.writeText(markerJson.toString())
|
|
289
|
+
FileOutputStream(bootMarker, true).use { it.fd.sync() }
|
|
290
|
+
Log.i(TAG, "Boot guard: attempt $attempts/$MAX_BOOT_ATTEMPTS for this bundle")
|
|
237
291
|
} catch (e: Exception) {
|
|
238
292
|
Log.e(TAG, "checkBootGuard() failed — reverting to be safe", e)
|
|
239
293
|
revert()
|
|
@@ -249,6 +303,18 @@ class SlotManager(private val filesDir: File) {
|
|
|
249
303
|
state.put("next", readMeta(nextSlot))
|
|
250
304
|
state.put("previous", readMeta(previousSlot))
|
|
251
305
|
state.put("bootMarkerPresent", bootMarker.exists())
|
|
306
|
+
// Consume-on-read: the orchestrator turns this into an AUTO_ROLLBACK
|
|
307
|
+
// event exactly once, then the record is gone. Leaving it in place would
|
|
308
|
+
// re-report the same rollback on every subsequent launch.
|
|
309
|
+
if (revertRecord.exists()) {
|
|
310
|
+
runCatching {
|
|
311
|
+
val rec = JSONObject(revertRecord.readText())
|
|
312
|
+
state.put("bootGuardReverted", true)
|
|
313
|
+
state.put("bootGuardRevertReason", rec.optString("reason", "boot_crash_guard"))
|
|
314
|
+
state.put("bootGuardRevertedAt", rec.optLong("at", 0L))
|
|
315
|
+
}
|
|
316
|
+
revertRecord.delete()
|
|
317
|
+
}
|
|
252
318
|
return state
|
|
253
319
|
}
|
|
254
320
|
|
|
@@ -25,6 +25,12 @@ RCT_EXTERN_METHOD(drainPendingCrash:(RCTPromiseResolveBlock)resolve
|
|
|
25
25
|
RCT_EXTERN_METHOD(crashNow:(RCTPromiseResolveBlock)resolve
|
|
26
26
|
reject:(RCTPromiseRejectBlock)reject)
|
|
27
27
|
|
|
28
|
+
// Selector must match the Swift @objc(setActiveSessionId:resolve:reject:) exactly — a mismatch here
|
|
29
|
+
// fails at call time, not build time, and the symptom is a crash with no session rather than an error.
|
|
30
|
+
RCT_EXTERN_METHOD(setActiveSessionId:(NSString *)sessionId
|
|
31
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
32
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
33
|
+
|
|
28
34
|
@end
|
|
29
35
|
|
|
30
36
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -39,6 +39,15 @@ import Foundation
|
|
|
39
39
|
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
40
40
|
"platform": "ios",
|
|
41
41
|
]
|
|
42
|
+
/**
|
|
43
|
+
* The session this crash belongs to, read from state written when the session began —
|
|
44
|
+
* never asked for now. The JS thread may already be gone, and a fatal handler must not
|
|
45
|
+
* call into it. Absent when no session had started yet, which the reader treats as
|
|
46
|
+
* unknown rather than guessing.
|
|
47
|
+
*/
|
|
48
|
+
if let sid = ScaleBunCrashStorage.activeSessionId() {
|
|
49
|
+
data["sessionId"] = sid
|
|
50
|
+
}
|
|
42
51
|
if let userInfo = exception.userInfo, !userInfo.isEmpty {
|
|
43
52
|
// Flatten userInfo to string-keyed strings to stay JSON-safe
|
|
44
53
|
var info: [String: String] = [:]
|
|
@@ -57,7 +66,11 @@ import Foundation
|
|
|
57
66
|
// Signal handlers — installs C-level handlers via ScaleBunCrashSignalBridge.
|
|
58
67
|
// Handlers write a minimal record (signal number + timestamp) using only
|
|
59
68
|
// async-signal-safe syscalls, then restore the default handler and re-raise.
|
|
60
|
-
ScaleBunCrashSignalBridgeInstall(
|
|
69
|
+
ScaleBunCrashSignalBridgeInstall(
|
|
70
|
+
ScaleBunCrashStorage.crashFilePathCString,
|
|
71
|
+
ScaleBunCrashStorage.sessionMarkerPathCString,
|
|
72
|
+
ScaleBunCrashStorage.sessionTmpPathCString
|
|
73
|
+
)
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/** For testing only — resets the installed flag. */
|
|
@@ -50,6 +50,20 @@ class ScaleBunCrashModule: NSObject, RCTBridgeModule {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Record which session is live, so a crash can name it.
|
|
55
|
+
*
|
|
56
|
+
* Once per session, never per event. It must be durable before the next line of app code runs,
|
|
57
|
+
* because the next line of app code is what might crash.
|
|
58
|
+
*/
|
|
59
|
+
@objc(setActiveSessionId:resolve:reject:)
|
|
60
|
+
func setActiveSessionId(_ sessionId: NSString?,
|
|
61
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
62
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
63
|
+
ScaleBunCrashStorage.setActiveSessionId(sessionId as String?)
|
|
64
|
+
resolve(true)
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
@objc(drainPendingCrash:reject:)
|
|
54
68
|
func drainPendingCrash(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
55
69
|
reject: @escaping RCTPromiseRejectBlock) {
|
|
@@ -15,4 +15,35 @@
|
|
|
15
15
|
* @param crashFilePath Null-terminated C string. Must remain valid for the
|
|
16
16
|
* lifetime of the process (use a static buffer).
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @param crashFilePath Where the crash record is written.
|
|
20
|
+
* @param sessionMarkerPath Where the active session id is published (read by the handler).
|
|
21
|
+
* @param sessionTmpPath Staging path for the atomic rename. MUST be on the same filesystem as
|
|
22
|
+
* sessionMarkerPath — rename is only atomic within one filesystem, and a
|
|
23
|
+
* cross-device rename fails outright, which would leave the marker stale.
|
|
24
|
+
*
|
|
25
|
+
* All three must remain valid for the process lifetime (use static buffers).
|
|
26
|
+
*/
|
|
27
|
+
void ScaleBunCrashSignalBridgeInstall(const char *crashFilePath,
|
|
28
|
+
const char *sessionMarkerPath,
|
|
29
|
+
const char *sessionTmpPath);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Publish the active session id so a signal crash can name the session it happened in.
|
|
33
|
+
*
|
|
34
|
+
* Call from NORMAL CODE only (once per session) — never from a signal handler. It writes a temp file
|
|
35
|
+
* and `rename(2)`s it over the marker; POSIX requires that rename be atomic, so a handler reading
|
|
36
|
+
* the marker sees either the previous complete id or the new complete id, never a mix.
|
|
37
|
+
*
|
|
38
|
+
* This replaced a shared static buffer published under a `volatile sig_atomic_t` length. That design
|
|
39
|
+
* could tear: overwriting an id IN PLACE with one of the SAME length left the length valid while the
|
|
40
|
+
* bytes were half-replaced, so the handler could read "sess-BBBAAAA…". Nothing about the atomicity
|
|
41
|
+
* of the length fixed that, because the length never changed.
|
|
42
|
+
*
|
|
43
|
+
* Pass NULL to clear.
|
|
44
|
+
*
|
|
45
|
+
* @param sessionId Null-terminated C string. Copied, so the caller's storage need not outlive the
|
|
46
|
+
* call. Characters outside [A-Za-z0-9_.:-] are dropped so the bytes are safe to
|
|
47
|
+
* emit inside a JSON string without escaping.
|
|
48
|
+
*/
|
|
49
|
+
void ScaleBunCrashSignalBridgeSetSessionId(const char *sessionId);
|