@grafana/faro-react-native 1.3.0 → 1.4.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/README.md +176 -30
- package/android/build.gradle +20 -15
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
- package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
- package/dist/cjs/config/getRNInstrumentations.js +6 -8
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/sampling.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/appState/index.js +8 -2
- package/dist/cjs/instrumentations/appState/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +8 -3
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/cjs/instrumentations/performance/index.js +2 -6
- package/dist/cjs/instrumentations/performance/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/index.js +214 -45
- package/dist/cjs/instrumentations/session/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
- package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
- package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
- package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/cjs/instrumentations/startup/index.js +2 -6
- package/dist/cjs/instrumentations/startup/index.js.map +1 -1
- package/dist/cjs/navigation/utils.js +3 -1
- package/dist/cjs/navigation/utils.js.map +1 -1
- package/dist/cjs/transports/fetch/index.js.map +1 -1
- package/dist/cjs/transports/fetch/transport.js +56 -25
- package/dist/cjs/transports/fetch/transport.js.map +1 -1
- package/dist/cjs/transports/fetch/types.js.map +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/cjs/transports/offline/OfflineCache.js +2 -2
- package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
- package/dist/cjs/transports/offline/transport.js +3 -4
- package/dist/cjs/transports/offline/transport.js.map +1 -1
- package/dist/cjs/userPersistence/UserPersistence.js +1 -2
- package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +6 -8
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/sampling.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
- package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/appState/index.js +8 -2
- package/dist/esm/instrumentations/appState/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +8 -3
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/esm/instrumentations/performance/index.js +2 -6
- package/dist/esm/instrumentations/performance/index.js.map +1 -1
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/index.js +207 -46
- package/dist/esm/instrumentations/session/index.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
- package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
- package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
- package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/esm/instrumentations/startup/index.js +2 -6
- package/dist/esm/instrumentations/startup/index.js.map +1 -1
- package/dist/esm/navigation/utils.js +2 -1
- package/dist/esm/navigation/utils.js.map +1 -1
- package/dist/esm/transports/fetch/index.js.map +1 -1
- package/dist/esm/transports/fetch/transport.js +38 -14
- package/dist/esm/transports/fetch/transport.js.map +1 -1
- package/dist/esm/transports/fetch/types.js.map +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/esm/transports/offline/OfflineCache.js +2 -2
- package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
- package/dist/esm/transports/offline/transport.js +3 -4
- package/dist/esm/transports/offline/transport.js.map +1 -1
- package/dist/esm/userPersistence/UserPersistence.js +1 -2
- package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
- package/dist/types/config/getRNInstrumentations.d.ts +1 -2
- package/dist/types/config/sampling.d.ts +0 -3
- package/dist/types/config/types.d.ts +5 -3
- package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrumentations/anr/instrumentation.d.ts +1 -1
- package/dist/types/instrumentations/appState/index.d.ts +1 -1
- package/dist/types/instrumentations/console/index.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/http/index.d.ts +1 -1
- package/dist/types/instrumentations/performance/index.d.ts +1 -3
- package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
- package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
- package/dist/types/instrumentations/session/index.d.ts +21 -2
- package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
- package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
- package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
- package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
- package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
- package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
- package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
- package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
- package/dist/types/instrumentations/startup/index.d.ts +3 -7
- package/dist/types/instrumentations/userActions/index.d.ts +1 -1
- package/dist/types/instrumentations/view/index.d.ts +1 -1
- package/dist/types/instrumentations/xhr/index.d.ts +1 -1
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
- package/dist/types/transports/console/transport.d.ts +1 -1
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +9 -2
- package/dist/types/transports/fetch/types.d.ts +5 -0
- package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
- package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
- package/dist/types/transports/offline/transport.d.ts +4 -5
- package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
- package/ios/FaroCrashReporter.swift +214 -21
- package/ios/FaroReactNative.swift +24 -0
- package/ios/FaroReactNativeModule.h +2 -1
- package/ios/FaroReactNativeModule.mm +121 -6
- package/ios/RefreshRateVitals.swift +34 -38
- package/package.json +8 -6
- package/src/config/getRNInstrumentations.ts +6 -8
- package/src/config/makeRNConfig.test.ts +6 -0
- package/src/config/sampling.ts +0 -3
- package/src/config/types.ts +5 -3
- package/src/dataCollection/DataCollectionPolicy.ts +1 -2
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/index.ts +8 -2
- package/src/instrumentations/anr/instrumentation.ts +8 -1
- package/src/instrumentations/appState/index.ts +10 -2
- package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
- package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
- package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
- package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/types.ts +7 -0
- package/src/instrumentations/errors/index.ts +12 -4
- package/src/instrumentations/errors/instrumentation.test.ts +35 -0
- package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
- package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
- package/src/instrumentations/frameMonitoring/types.ts +4 -6
- package/src/instrumentations/performance/index.ts +2 -6
- package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
- package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
- package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
- package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
- package/src/instrumentations/session/directSessionActivity.ts +36 -0
- package/src/instrumentations/session/index.ts +269 -46
- package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
- package/src/instrumentations/session/sessionActivity.test.ts +68 -0
- package/src/instrumentations/session/sessionActivity.ts +58 -0
- package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
- package/src/instrumentations/session/sessionAttributes.ts +12 -6
- package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
- package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
- package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
- package/src/instrumentations/session/sessionManager/types.ts +8 -0
- package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
- package/src/instrumentations/session/sessionProcess.test.ts +136 -0
- package/src/instrumentations/session/sessionProcess.ts +114 -0
- package/src/instrumentations/session/startNewSession.test.ts +337 -0
- package/src/instrumentations/startup/index.ts +2 -6
- package/src/navigation/utils.ts +3 -1
- package/src/transports/fetch/index.ts +6 -1
- package/src/transports/fetch/transport.test.ts +86 -2
- package/src/transports/fetch/transport.ts +38 -16
- package/src/transports/fetch/types.ts +7 -0
- package/src/transports/offline/ConnectivityService.ts +1 -1
- package/src/transports/offline/OfflineCache.ts +2 -2
- package/src/transports/offline/transport.ts +3 -4
- package/src/userPersistence/UserPersistence.ts +1 -2
- package/dist/cjs/utils/throttle.js +0 -42
- package/dist/cjs/utils/throttle.js.map +0 -1
- package/dist/esm/utils/throttle.js +0 -34
- package/dist/esm/utils/throttle.js.map +0 -1
- package/dist/types/utils/throttle.d.ts +0 -9
- package/src/utils/throttle.test.ts +0 -141
- package/src/utils/throttle.ts +0 -34
|
@@ -34,6 +34,18 @@ object FaroCrashReporter {
|
|
|
34
34
|
private const val PREFS_NAME = "com.grafana.faro.crash_reporter"
|
|
35
35
|
private const val KEY_LAST_PROCESSED_TIMESTAMP = "last_processed_timestamp"
|
|
36
36
|
|
|
37
|
+
private data class ExitGroupKey(
|
|
38
|
+
val timestampMs: Long,
|
|
39
|
+
val pid: Int,
|
|
40
|
+
val processName: String,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
private sealed interface ExportResult {
|
|
44
|
+
data class Report(val json: String) : ExportResult
|
|
45
|
+
data object Ignored : ExportResult
|
|
46
|
+
data object Failed : ExportResult
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
/**
|
|
38
50
|
* Persists a text tombstone backtrace for the next [REASON_CRASH_NATIVE] replay.
|
|
39
51
|
*
|
|
@@ -50,13 +62,18 @@ object FaroCrashReporter {
|
|
|
50
62
|
*
|
|
51
63
|
* Returns a list of JSON strings, each representing a crash report.
|
|
52
64
|
* The JSON format matches the iOS implementation for consistency.
|
|
53
|
-
*
|
|
65
|
+
* Retrieval is non-destructive. Reports remain pending until JavaScript
|
|
66
|
+
* acknowledges successful delivery or an intentional local filter.
|
|
54
67
|
*
|
|
55
68
|
* @param context Android context
|
|
56
69
|
* @return List of crash report JSON strings, or null if no crashes or unsupported API
|
|
57
70
|
*/
|
|
71
|
+
@JvmOverloads
|
|
58
72
|
@JvmStatic
|
|
59
|
-
fun getCrashReports(
|
|
73
|
+
fun getCrashReports(
|
|
74
|
+
context: Context,
|
|
75
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
76
|
+
): List<String>? {
|
|
60
77
|
FaroUncaughtExceptionHandler.install(context)
|
|
61
78
|
// ApplicationExitInfo requires Android 11 (API 30)
|
|
62
79
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
@@ -76,59 +93,126 @@ object FaroCrashReporter {
|
|
|
76
93
|
return null
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
//
|
|
96
|
+
// Preserve the old watermark as a one-way migration boundary. New reports
|
|
97
|
+
// use stable IDs and are acknowledged only after delivery.
|
|
80
98
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
81
|
-
val
|
|
99
|
+
val legacyLastProcessedTimestamp = prefs.getLong(KEY_LAST_PROCESSED_TIMESTAMP, 0)
|
|
82
100
|
|
|
83
|
-
val pendingTrace = FaroCrashTraceCache.peekPendingCrashTrace(context)
|
|
84
|
-
var usedPendingTrace = false
|
|
101
|
+
val pendingTrace = FaroCrashTraceCache.peekPendingCrashTrace(context, nowMs)
|
|
85
102
|
|
|
86
103
|
val crashReports = mutableListOf<String>()
|
|
87
|
-
var latestTimestamp = lastProcessedTimestamp
|
|
88
|
-
val reportedTimestamps = mutableSetOf<Long>()
|
|
89
104
|
|
|
90
105
|
// Android can return multiple ApplicationExitInfo rows for one fatal exit
|
|
91
106
|
// (e.g. REASON_CRASH plus a companion row with no trace). Collapse to one
|
|
92
|
-
// report per timestamp and share the UncaughtExceptionHandler cache.
|
|
107
|
+
// report per process/timestamp and share the UncaughtExceptionHandler cache.
|
|
93
108
|
val candidates = exitInfoList
|
|
94
|
-
.filter {
|
|
95
|
-
|
|
109
|
+
.filter {
|
|
110
|
+
it.timestamp > legacyLastProcessedTimestamp &&
|
|
111
|
+
isCrashReason(it.reason)
|
|
112
|
+
}
|
|
113
|
+
.groupBy {
|
|
114
|
+
ExitGroupKey(
|
|
115
|
+
timestampMs = it.timestamp,
|
|
116
|
+
pid = it.pid,
|
|
117
|
+
processName = it.processName.orEmpty(),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
96
120
|
.mapValues { (_, exits) -> pickBestExitInfo(exits) }
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
.entries
|
|
122
|
+
.sortedBy { it.key.timestampMs }
|
|
123
|
+
|
|
124
|
+
for ((_, exitInfo) in candidates) {
|
|
125
|
+
val reportId = FaroCrashIds.reportId(
|
|
126
|
+
packageName = context.packageName,
|
|
127
|
+
timestampMs = exitInfo.timestamp,
|
|
128
|
+
pid = exitInfo.pid,
|
|
129
|
+
processName = exitInfo.processName.orEmpty(),
|
|
130
|
+
)
|
|
131
|
+
if (FaroCrashSessionStore.isReportAcknowledged(context, reportId, nowMs)) {
|
|
101
132
|
continue
|
|
102
133
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
if (!isWithinReplayWindow(exitInfo.timestamp, nowMs)) {
|
|
135
|
+
FaroCrashSessionStore.acknowledgeReports(
|
|
136
|
+
context = context,
|
|
137
|
+
reportIds = listOf(reportId),
|
|
138
|
+
nowMs = nowMs,
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
106
141
|
}
|
|
107
142
|
|
|
108
|
-
val
|
|
143
|
+
val sessionContext = FaroCrashSessionStore.contextForPendingReport(context, reportId, nowMs)
|
|
144
|
+
?: FaroCrashSessionStore.findMatchingContext(
|
|
145
|
+
context = context,
|
|
146
|
+
crashTimestampMs = exitInfo.timestamp,
|
|
147
|
+
pid = exitInfo.pid,
|
|
148
|
+
processName = exitInfo.processName.orEmpty(),
|
|
149
|
+
nowMs = nowMs,
|
|
150
|
+
)
|
|
151
|
+
var usedPendingTrace = false
|
|
152
|
+
|
|
153
|
+
val exportResult = exportExitInfoAsJSON(
|
|
154
|
+
context = context,
|
|
155
|
+
exitInfo = exitInfo,
|
|
156
|
+
reportId = reportId,
|
|
157
|
+
sessionContext = sessionContext,
|
|
158
|
+
pendingTrace = pendingTrace,
|
|
159
|
+
) { consumed ->
|
|
109
160
|
if (consumed) {
|
|
110
161
|
usedPendingTrace = true
|
|
111
162
|
}
|
|
112
163
|
}
|
|
113
164
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
when (exportResult) {
|
|
166
|
+
is ExportResult.Report -> {
|
|
167
|
+
FaroCrashSessionStore.rememberPendingReport(
|
|
168
|
+
context = context,
|
|
169
|
+
reportId = reportId,
|
|
170
|
+
crashTimestampMs = exitInfo.timestamp,
|
|
171
|
+
sessionContext = sessionContext,
|
|
172
|
+
usesPendingTrace = usedPendingTrace,
|
|
173
|
+
nowMs = nowMs,
|
|
174
|
+
)
|
|
175
|
+
crashReports.add(exportResult.json)
|
|
176
|
+
}
|
|
177
|
+
ExportResult.Ignored -> {
|
|
178
|
+
// Prevent intentionally filtered companion or ANR rows from
|
|
179
|
+
// being reconsidered on every subsequent app launch.
|
|
180
|
+
FaroCrashSessionStore.acknowledgeReports(
|
|
181
|
+
context = context,
|
|
182
|
+
reportIds = listOf(reportId),
|
|
183
|
+
nowMs = nowMs,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
ExportResult.Failed -> Unit
|
|
117
187
|
}
|
|
118
188
|
}
|
|
119
189
|
|
|
120
|
-
if (
|
|
190
|
+
return if (crashReports.isEmpty()) null else crashReports
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@JvmStatic
|
|
194
|
+
fun acknowledgeCrashReports(
|
|
195
|
+
context: Context,
|
|
196
|
+
reportIds: Collection<String>,
|
|
197
|
+
): Boolean {
|
|
198
|
+
val result = FaroCrashSessionStore.acknowledgeReports(context, reportIds)
|
|
199
|
+
if (!result.persisted) {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
if (result.clearPendingTrace) {
|
|
121
203
|
FaroCrashTraceCache.clearPendingCrashTrace(context)
|
|
122
204
|
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
123
207
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
208
|
+
internal fun isWithinReplayWindow(
|
|
209
|
+
crashTimestampMs: Long,
|
|
210
|
+
nowMs: Long,
|
|
211
|
+
): Boolean {
|
|
212
|
+
if (crashTimestampMs <= 0L || crashTimestampMs > nowMs) {
|
|
213
|
+
return false
|
|
129
214
|
}
|
|
130
|
-
|
|
131
|
-
return if (crashReports.isEmpty()) null else crashReports
|
|
215
|
+
return nowMs - crashTimestampMs <= FaroCrashSessionStore.MAX_CONTEXT_AGE_MS
|
|
132
216
|
}
|
|
133
217
|
|
|
134
218
|
/**
|
|
@@ -188,16 +272,20 @@ object FaroCrashReporter {
|
|
|
188
272
|
private fun exportExitInfoAsJSON(
|
|
189
273
|
context: Context,
|
|
190
274
|
exitInfo: ApplicationExitInfo,
|
|
275
|
+
reportId: String,
|
|
276
|
+
sessionContext: FaroCrashSessionStore.SessionContext?,
|
|
191
277
|
pendingTrace: FaroCrashTraceCache.PendingTrace?,
|
|
192
278
|
onPendingTraceUsed: (Boolean) -> Unit,
|
|
193
|
-
):
|
|
279
|
+
): ExportResult {
|
|
194
280
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
195
|
-
return
|
|
281
|
+
return ExportResult.Failed
|
|
196
282
|
}
|
|
197
283
|
|
|
198
284
|
return try {
|
|
199
285
|
val json = JSONObject()
|
|
200
286
|
|
|
287
|
+
json.put("reportId", reportId)
|
|
288
|
+
|
|
201
289
|
// Reason - maps to crash type
|
|
202
290
|
json.put("reason", getReasonString(exitInfo.reason))
|
|
203
291
|
|
|
@@ -220,9 +308,17 @@ object FaroCrashReporter {
|
|
|
220
308
|
// Importance (Android-specific)
|
|
221
309
|
json.put("importance", exitInfo.importance)
|
|
222
310
|
|
|
311
|
+
if (sessionContext != null) {
|
|
312
|
+
json.put("sessionId", sessionContext.sessionId)
|
|
313
|
+
sessionContext.isSampled?.let { json.put("isSampled", it) }
|
|
314
|
+
sessionContext.appVersion?.let { json.put("appVersion", it) }
|
|
315
|
+
sessionContext.appRelease?.let { json.put("appRelease", it) }
|
|
316
|
+
sessionContext.appBundleId?.let { json.put("appBundleId", it) }
|
|
317
|
+
}
|
|
318
|
+
|
|
223
319
|
if (isAnrTimeoutDescription(rawDescription)) {
|
|
224
320
|
// ANRInstrumentation reports these with type ANR.
|
|
225
|
-
return
|
|
321
|
+
return ExportResult.Ignored
|
|
226
322
|
}
|
|
227
323
|
|
|
228
324
|
val parsedExitTrace = readExitTrace(exitInfo)
|
|
@@ -239,7 +335,7 @@ object FaroCrashReporter {
|
|
|
239
335
|
) {
|
|
240
336
|
// Suppress empty generic crash rows that duplicate a nearby ANR report.
|
|
241
337
|
// Real crashes with a trace still surface even when an ANR happened nearby.
|
|
242
|
-
return
|
|
338
|
+
return ExportResult.Ignored
|
|
243
339
|
}
|
|
244
340
|
|
|
245
341
|
if (trace.isNotEmpty()) {
|
|
@@ -270,12 +366,12 @@ object FaroCrashReporter {
|
|
|
270
366
|
"[Faro crash native] Skipping CRASH_NATIVE without tombstone trace (traceInputStream was null and no cached native backtrace)",
|
|
271
367
|
)
|
|
272
368
|
}
|
|
273
|
-
return
|
|
369
|
+
return ExportResult.Ignored
|
|
274
370
|
}
|
|
275
371
|
|
|
276
|
-
json.toString()
|
|
277
|
-
} catch (
|
|
278
|
-
|
|
372
|
+
ExportResult.Report(json.toString())
|
|
373
|
+
} catch (_: Exception) {
|
|
374
|
+
ExportResult.Failed
|
|
279
375
|
}
|
|
280
376
|
}
|
|
281
377
|
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
package com.grafana.faro.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Process
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persists the minimum process/session context needed to associate a historical
|
|
10
|
+
* ApplicationExitInfo crash with the Faro session that was active before process death.
|
|
11
|
+
*/
|
|
12
|
+
internal object FaroCrashSessionStore {
|
|
13
|
+
// Keep aligned with AndroidCrashReportingInstrumentation.MAX_REPLAY_AGE_MS.
|
|
14
|
+
internal const val MAX_CONTEXT_AGE_MS = 7L * 24L * 60L * 60L * 1000L
|
|
15
|
+
|
|
16
|
+
private const val PREFS_NAME = "com.grafana.faro.crash_session_store"
|
|
17
|
+
private const val KEY_SESSION_CONTEXTS = "session_contexts"
|
|
18
|
+
private const val KEY_PENDING_REPORTS = "pending_reports"
|
|
19
|
+
private const val KEY_ACKNOWLEDGED_REPORTS = "acknowledged_reports"
|
|
20
|
+
private const val MAX_SESSION_CONTEXTS = 100
|
|
21
|
+
private const val MAX_PENDING_REPORTS = 50
|
|
22
|
+
private const val MAX_ACKNOWLEDGED_REPORTS = 100
|
|
23
|
+
private const val SCHEMA_VERSION = 1
|
|
24
|
+
|
|
25
|
+
data class SessionContext(
|
|
26
|
+
val id: String,
|
|
27
|
+
val sessionId: String,
|
|
28
|
+
val activatedAtMs: Long,
|
|
29
|
+
val updatedAtMs: Long,
|
|
30
|
+
val pid: Int,
|
|
31
|
+
val processName: String,
|
|
32
|
+
val isSampled: Boolean?,
|
|
33
|
+
val appVersion: String?,
|
|
34
|
+
val appRelease: String?,
|
|
35
|
+
val appBundleId: String?,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
private data class PendingReport(
|
|
39
|
+
val reportId: String,
|
|
40
|
+
val contextId: String?,
|
|
41
|
+
val crashTimestampMs: Long,
|
|
42
|
+
val usesPendingTrace: Boolean,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
private data class AcknowledgedReport(
|
|
46
|
+
val reportId: String,
|
|
47
|
+
val acknowledgedAtMs: Long,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
private data class State(
|
|
51
|
+
val contexts: MutableList<SessionContext>,
|
|
52
|
+
val pendingReports: MutableList<PendingReport>,
|
|
53
|
+
val acknowledgedReports: MutableList<AcknowledgedReport>,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
data class AcknowledgeResult(
|
|
57
|
+
val persisted: Boolean,
|
|
58
|
+
val clearPendingTrace: Boolean,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@Synchronized
|
|
62
|
+
fun recordSessionContext(
|
|
63
|
+
context: Context,
|
|
64
|
+
sessionId: String,
|
|
65
|
+
activatedAtMs: Long,
|
|
66
|
+
isSampled: Boolean?,
|
|
67
|
+
appVersion: String?,
|
|
68
|
+
appRelease: String?,
|
|
69
|
+
appBundleId: String?,
|
|
70
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
71
|
+
pid: Int = Process.myPid(),
|
|
72
|
+
processName: String = currentProcessName(context.applicationContext),
|
|
73
|
+
): Boolean {
|
|
74
|
+
val normalizedSessionId = sessionId.trim()
|
|
75
|
+
if (normalizedSessionId.isEmpty() || activatedAtMs <= 0L) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
val appContext = context.applicationContext
|
|
80
|
+
val normalizedProcessName = processName.trim()
|
|
81
|
+
if (pid <= 0 || normalizedProcessName.isEmpty()) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
85
|
+
val state = readState(prefs, nowMs)
|
|
86
|
+
val existingIndex = state.contexts.indexOfFirst {
|
|
87
|
+
it.sessionId == normalizedSessionId &&
|
|
88
|
+
it.pid == pid &&
|
|
89
|
+
it.processName == normalizedProcessName
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
val existing = state.contexts.getOrNull(existingIndex)
|
|
93
|
+
val firstActivation = existing?.activatedAtMs ?: activatedAtMs
|
|
94
|
+
val sessionContext = SessionContext(
|
|
95
|
+
id = existing?.id ?: FaroCrashIds.sessionContextId(
|
|
96
|
+
normalizedSessionId,
|
|
97
|
+
firstActivation,
|
|
98
|
+
pid,
|
|
99
|
+
normalizedProcessName,
|
|
100
|
+
),
|
|
101
|
+
sessionId = normalizedSessionId,
|
|
102
|
+
activatedAtMs = firstActivation,
|
|
103
|
+
updatedAtMs = nowMs,
|
|
104
|
+
pid = pid,
|
|
105
|
+
processName = normalizedProcessName,
|
|
106
|
+
isSampled = isSampled,
|
|
107
|
+
appVersion = appVersion.normalized(),
|
|
108
|
+
appRelease = appRelease.normalized(),
|
|
109
|
+
appBundleId = appBundleId.normalized(),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (existingIndex >= 0) {
|
|
113
|
+
state.contexts[existingIndex] = sessionContext
|
|
114
|
+
} else {
|
|
115
|
+
state.contexts.add(sessionContext)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
state.contexts.sortBy { it.activatedAtMs }
|
|
119
|
+
trimSessionContexts(state)
|
|
120
|
+
return writeState(prefs, state)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Synchronized
|
|
124
|
+
fun findMatchingContext(
|
|
125
|
+
context: Context,
|
|
126
|
+
crashTimestampMs: Long,
|
|
127
|
+
pid: Int,
|
|
128
|
+
processName: String,
|
|
129
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
130
|
+
): SessionContext? {
|
|
131
|
+
if (crashTimestampMs <= 0L) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
val normalizedProcessName = processName.trim()
|
|
136
|
+
if (pid <= 0 && normalizedProcessName.isEmpty()) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
141
|
+
return readState(prefs, nowMs).contexts
|
|
142
|
+
.asSequence()
|
|
143
|
+
.filter { it.activatedAtMs <= crashTimestampMs }
|
|
144
|
+
.filter { pid <= 0 || it.pid == pid }
|
|
145
|
+
.filter { normalizedProcessName.isEmpty() || it.processName == normalizedProcessName }
|
|
146
|
+
.maxByOrNull { it.activatedAtMs }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Synchronized
|
|
150
|
+
fun contextForPendingReport(
|
|
151
|
+
context: Context,
|
|
152
|
+
reportId: String,
|
|
153
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
154
|
+
): SessionContext? {
|
|
155
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
156
|
+
val state = readState(prefs, nowMs)
|
|
157
|
+
val contextId = state.pendingReports.firstOrNull { it.reportId == reportId }?.contextId ?: return null
|
|
158
|
+
return state.contexts.firstOrNull { it.id == contextId }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Synchronized
|
|
162
|
+
fun rememberPendingReport(
|
|
163
|
+
context: Context,
|
|
164
|
+
reportId: String,
|
|
165
|
+
crashTimestampMs: Long,
|
|
166
|
+
sessionContext: SessionContext?,
|
|
167
|
+
usesPendingTrace: Boolean,
|
|
168
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
169
|
+
) {
|
|
170
|
+
if (reportId.isBlank() || crashTimestampMs <= 0L) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
175
|
+
val state = readState(prefs, nowMs)
|
|
176
|
+
val existingIndex = state.pendingReports.indexOfFirst { it.reportId == reportId }
|
|
177
|
+
val existing = state.pendingReports.getOrNull(existingIndex)
|
|
178
|
+
val pendingReport = PendingReport(
|
|
179
|
+
reportId = reportId,
|
|
180
|
+
contextId = existing?.contextId ?: sessionContext?.id,
|
|
181
|
+
crashTimestampMs = crashTimestampMs,
|
|
182
|
+
usesPendingTrace = existing?.usesPendingTrace == true || usesPendingTrace,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (existingIndex >= 0) {
|
|
186
|
+
state.pendingReports[existingIndex] = pendingReport
|
|
187
|
+
} else {
|
|
188
|
+
state.pendingReports.add(pendingReport)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
state.pendingReports.sortBy { it.crashTimestampMs }
|
|
192
|
+
trimOldest(state.pendingReports, MAX_PENDING_REPORTS)
|
|
193
|
+
writeState(prefs, state)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Synchronized
|
|
197
|
+
fun isReportAcknowledged(
|
|
198
|
+
context: Context,
|
|
199
|
+
reportId: String,
|
|
200
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
201
|
+
): Boolean {
|
|
202
|
+
if (reportId.isBlank()) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
206
|
+
return readState(prefs, nowMs).acknowledgedReports.any { it.reportId == reportId }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@Synchronized
|
|
210
|
+
fun acknowledgeReports(
|
|
211
|
+
context: Context,
|
|
212
|
+
reportIds: Collection<String>,
|
|
213
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
214
|
+
): AcknowledgeResult {
|
|
215
|
+
val normalizedIds = reportIds.map(String::trim).filter(String::isNotEmpty).toSet()
|
|
216
|
+
if (normalizedIds.isEmpty()) {
|
|
217
|
+
return AcknowledgeResult(persisted = true, clearPendingTrace = false)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
221
|
+
val state = readState(prefs, nowMs)
|
|
222
|
+
val acknowledgedMappings = state.pendingReports.filter { it.reportId in normalizedIds }
|
|
223
|
+
val contextIds = acknowledgedMappings.mapNotNull { it.contextId }.toSet()
|
|
224
|
+
val acknowledgedPendingTrace = acknowledgedMappings.any { it.usesPendingTrace }
|
|
225
|
+
|
|
226
|
+
state.pendingReports.removeAll { it.reportId in normalizedIds }
|
|
227
|
+
val remainingContextIds = state.pendingReports.mapNotNullTo(mutableSetOf()) { it.contextId }
|
|
228
|
+
state.contexts.removeAll { it.id in contextIds && it.id !in remainingContextIds }
|
|
229
|
+
val clearPendingTrace = acknowledgedPendingTrace && state.pendingReports.none { it.usesPendingTrace }
|
|
230
|
+
|
|
231
|
+
state.acknowledgedReports.removeAll { it.reportId in normalizedIds }
|
|
232
|
+
normalizedIds.forEach { reportId ->
|
|
233
|
+
state.acknowledgedReports.add(AcknowledgedReport(reportId, nowMs))
|
|
234
|
+
}
|
|
235
|
+
trimOldest(state.acknowledgedReports, MAX_ACKNOWLEDGED_REPORTS)
|
|
236
|
+
val persisted = writeState(prefs, state)
|
|
237
|
+
|
|
238
|
+
return AcknowledgeResult(
|
|
239
|
+
persisted = persisted,
|
|
240
|
+
clearPendingTrace = persisted && clearPendingTrace,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Synchronized
|
|
245
|
+
internal fun sessionContextsForTest(
|
|
246
|
+
context: Context,
|
|
247
|
+
nowMs: Long,
|
|
248
|
+
): List<SessionContext> {
|
|
249
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
250
|
+
return readState(prefs, nowMs).contexts
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun currentProcessName(context: Context): String {
|
|
254
|
+
return FaroSessionProcess.identifier()
|
|
255
|
+
?: context.applicationInfo.processName.orEmpty().ifBlank { context.packageName }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun readState(
|
|
259
|
+
prefs: android.content.SharedPreferences,
|
|
260
|
+
nowMs: Long,
|
|
261
|
+
): State {
|
|
262
|
+
val cutoff = nowMs - MAX_CONTEXT_AGE_MS
|
|
263
|
+
val contexts = readArray(prefs, KEY_SESSION_CONTEXTS)
|
|
264
|
+
.mapNotNull(::sessionContextFromJson)
|
|
265
|
+
.filterTo(mutableListOf()) { it.updatedAtMs >= cutoff }
|
|
266
|
+
val pendingReports = readArray(prefs, KEY_PENDING_REPORTS)
|
|
267
|
+
.mapNotNull(::pendingReportFromJson)
|
|
268
|
+
.filterTo(mutableListOf()) { it.crashTimestampMs >= cutoff }
|
|
269
|
+
val acknowledgedReports = readArray(prefs, KEY_ACKNOWLEDGED_REPORTS)
|
|
270
|
+
.mapNotNull(::acknowledgedReportFromJson)
|
|
271
|
+
.filterTo(mutableListOf()) { it.acknowledgedAtMs >= cutoff }
|
|
272
|
+
|
|
273
|
+
return State(contexts, pendingReports, acknowledgedReports)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun writeState(
|
|
277
|
+
prefs: android.content.SharedPreferences,
|
|
278
|
+
state: State,
|
|
279
|
+
): Boolean {
|
|
280
|
+
return prefs.edit()
|
|
281
|
+
.putString(KEY_SESSION_CONTEXTS, JSONArray(state.contexts.map(::sessionContextToJson)).toString())
|
|
282
|
+
.putString(KEY_PENDING_REPORTS, JSONArray(state.pendingReports.map(::pendingReportToJson)).toString())
|
|
283
|
+
.putString(
|
|
284
|
+
KEY_ACKNOWLEDGED_REPORTS,
|
|
285
|
+
JSONArray(state.acknowledgedReports.map(::acknowledgedReportToJson)).toString(),
|
|
286
|
+
)
|
|
287
|
+
.commit()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private fun readArray(
|
|
291
|
+
prefs: android.content.SharedPreferences,
|
|
292
|
+
key: String,
|
|
293
|
+
): List<JSONObject> {
|
|
294
|
+
val array = try {
|
|
295
|
+
JSONArray(prefs.getString(key, "[]") ?: "[]")
|
|
296
|
+
} catch (_: Exception) {
|
|
297
|
+
JSONArray()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return buildList {
|
|
301
|
+
for (index in 0 until array.length()) {
|
|
302
|
+
array.optJSONObject(index)?.let(::add)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private fun sessionContextToJson(context: SessionContext): JSONObject {
|
|
308
|
+
return JSONObject()
|
|
309
|
+
.put("schemaVersion", SCHEMA_VERSION)
|
|
310
|
+
.put("id", context.id)
|
|
311
|
+
.put("sessionId", context.sessionId)
|
|
312
|
+
.put("activatedAt", context.activatedAtMs)
|
|
313
|
+
.put("updatedAt", context.updatedAtMs)
|
|
314
|
+
.put("pid", context.pid)
|
|
315
|
+
.put("processName", context.processName)
|
|
316
|
+
.putOptional("isSampled", context.isSampled)
|
|
317
|
+
.putOptional("appVersion", context.appVersion)
|
|
318
|
+
.putOptional("appRelease", context.appRelease)
|
|
319
|
+
.putOptional("appBundleId", context.appBundleId)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private fun sessionContextFromJson(json: JSONObject): SessionContext? {
|
|
323
|
+
if (json.optInt("schemaVersion", 0) != SCHEMA_VERSION) {
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
val id = json.optString("id").trim()
|
|
328
|
+
val sessionId = json.optString("sessionId").trim()
|
|
329
|
+
val activatedAtMs = json.optLong("activatedAt", 0L)
|
|
330
|
+
val updatedAtMs = json.optLong("updatedAt", activatedAtMs)
|
|
331
|
+
val pid = json.optInt("pid", 0)
|
|
332
|
+
val processName = json.optString("processName").trim()
|
|
333
|
+
if (
|
|
334
|
+
id.isEmpty() ||
|
|
335
|
+
sessionId.isEmpty() ||
|
|
336
|
+
activatedAtMs <= 0L ||
|
|
337
|
+
updatedAtMs <= 0L ||
|
|
338
|
+
pid <= 0 ||
|
|
339
|
+
processName.isEmpty()
|
|
340
|
+
) {
|
|
341
|
+
return null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return SessionContext(
|
|
345
|
+
id = id,
|
|
346
|
+
sessionId = sessionId,
|
|
347
|
+
activatedAtMs = activatedAtMs,
|
|
348
|
+
updatedAtMs = updatedAtMs,
|
|
349
|
+
pid = pid,
|
|
350
|
+
processName = processName,
|
|
351
|
+
isSampled = json.optionalBoolean("isSampled"),
|
|
352
|
+
appVersion = json.optionalString("appVersion"),
|
|
353
|
+
appRelease = json.optionalString("appRelease"),
|
|
354
|
+
appBundleId = json.optionalString("appBundleId"),
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private fun pendingReportToJson(report: PendingReport): JSONObject {
|
|
359
|
+
return JSONObject()
|
|
360
|
+
.put("reportId", report.reportId)
|
|
361
|
+
.putOptional("contextId", report.contextId)
|
|
362
|
+
.put("crashTimestamp", report.crashTimestampMs)
|
|
363
|
+
.put("usesPendingTrace", report.usesPendingTrace)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private fun pendingReportFromJson(json: JSONObject): PendingReport? {
|
|
367
|
+
val reportId = json.optString("reportId").trim()
|
|
368
|
+
val crashTimestampMs = json.optLong("crashTimestamp", 0L)
|
|
369
|
+
if (reportId.isEmpty() || crashTimestampMs <= 0L) {
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
return PendingReport(
|
|
373
|
+
reportId = reportId,
|
|
374
|
+
contextId = json.optionalString("contextId"),
|
|
375
|
+
crashTimestampMs = crashTimestampMs,
|
|
376
|
+
usesPendingTrace = json.optBoolean("usesPendingTrace", false),
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private fun acknowledgedReportToJson(report: AcknowledgedReport): JSONObject {
|
|
381
|
+
return JSONObject()
|
|
382
|
+
.put("reportId", report.reportId)
|
|
383
|
+
.put("acknowledgedAt", report.acknowledgedAtMs)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private fun acknowledgedReportFromJson(json: JSONObject): AcknowledgedReport? {
|
|
387
|
+
val reportId = json.optString("reportId").trim()
|
|
388
|
+
val acknowledgedAtMs = json.optLong("acknowledgedAt", 0L)
|
|
389
|
+
if (reportId.isEmpty() || acknowledgedAtMs <= 0L) {
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
return AcknowledgedReport(reportId, acknowledgedAtMs)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private fun JSONObject.putOptional(key: String, value: Any?): JSONObject {
|
|
396
|
+
if (value != null) {
|
|
397
|
+
put(key, value)
|
|
398
|
+
}
|
|
399
|
+
return this
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private fun JSONObject.optionalString(key: String): String? {
|
|
403
|
+
if (!has(key) || isNull(key)) {
|
|
404
|
+
return null
|
|
405
|
+
}
|
|
406
|
+
return optString(key).normalized()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private fun JSONObject.optionalBoolean(key: String): Boolean? {
|
|
410
|
+
if (!has(key) || isNull(key)) {
|
|
411
|
+
return null
|
|
412
|
+
}
|
|
413
|
+
return optBoolean(key)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private fun String?.normalized(): String? {
|
|
417
|
+
return this?.trim()?.takeIf(String::isNotEmpty)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private fun trimSessionContexts(state: State) {
|
|
421
|
+
val referencedContextIds = state.pendingReports.mapNotNullTo(mutableSetOf()) { it.contextId }
|
|
422
|
+
while (state.contexts.size > MAX_SESSION_CONTEXTS) {
|
|
423
|
+
val removableIndex = state.contexts.indexOfFirst { it.id !in referencedContextIds }
|
|
424
|
+
state.contexts.removeAt(if (removableIndex >= 0) removableIndex else 0)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private fun <T> trimOldest(items: MutableList<T>, maximumSize: Int) {
|
|
429
|
+
while (items.size > maximumSize) {
|
|
430
|
+
items.removeAt(0)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|