@rejourneyco/react-native 1.0.7 → 1.0.9
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 +1 -1
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
- package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +202 -133
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +29 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +241 -147
- package/ios/Recording/SegmentDispatcher.swift +155 -13
- package/ios/Recording/SpecialCases.swift +614 -0
- package/ios/Recording/StabilityMonitor.swift +42 -34
- package/ios/Recording/TelemetryPipeline.swift +38 -3
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +104 -28
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +32 -20
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/constants.js +2 -2
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/commonjs/sdk/utils.js +1 -1
- package/lib/module/index.js +32 -20
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/constants.js +2 -2
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/module/sdk/utils.js +1 -1
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/sdk/constants.d.ts +2 -2
- package/lib/typescript/types/index.d.ts +15 -8
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +46 -29
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/constants.ts +2 -2
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/sdk/utils.ts +1 -1
- package/src/types/index.ts +16 -9
|
@@ -40,20 +40,20 @@ import java.util.*
|
|
|
40
40
|
* Android implementation aligned with iOS ReplayOrchestrator.swift
|
|
41
41
|
*/
|
|
42
42
|
class ReplayOrchestrator private constructor(private val context: Context) {
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
companion object {
|
|
45
45
|
@Volatile
|
|
46
46
|
private var instance: ReplayOrchestrator? = null
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
fun getInstance(context: Context): ReplayOrchestrator {
|
|
49
49
|
return instance ?: synchronized(this) {
|
|
50
50
|
instance ?: ReplayOrchestrator(context.applicationContext).also { instance = it }
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
val shared: ReplayOrchestrator?
|
|
55
55
|
get() = instance
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
// Process start time for app startup tracking
|
|
58
58
|
private val processStartTime: Long by lazy {
|
|
59
59
|
try {
|
|
@@ -72,13 +72,13 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
var apiToken: String? = null
|
|
77
77
|
var replayId: String? = null
|
|
78
78
|
var replayStartMs: Long = 0
|
|
79
79
|
var deferredUploadMode = false
|
|
80
80
|
var frameBundleSize: Int = 5
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
var serverEndpoint: String
|
|
83
83
|
get() = TelemetryPipeline.shared?.endpoint ?: "https://api.rejourney.co"
|
|
84
84
|
set(value) {
|
|
@@ -86,8 +86,8 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
86
86
|
SegmentDispatcher.shared.endpoint = value
|
|
87
87
|
DeviceRegistrar.shared?.endpoint = value
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
var snapshotInterval: Double = 0
|
|
89
|
+
|
|
90
|
+
var snapshotInterval: Double = 1.0
|
|
91
91
|
var compressionLevel: Double = 0.5
|
|
92
92
|
var visualCaptureEnabled: Boolean = true
|
|
93
93
|
var interactionCaptureEnabled: Boolean = true
|
|
@@ -99,7 +99,7 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
99
99
|
var hierarchyCaptureInterval: Double = 2.0
|
|
100
100
|
var currentScreenName: String? = null
|
|
101
101
|
private set
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
// Remote config from backend (set via setRemoteConfig before session start)
|
|
104
104
|
var remoteRejourneyEnabled: Boolean = true
|
|
105
105
|
private set
|
|
@@ -109,7 +109,7 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
109
109
|
private set
|
|
110
110
|
var remoteMaxRecordingMinutes: Int = 10
|
|
111
111
|
private set
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Network state tracking
|
|
114
114
|
var currentNetworkType: String = "unknown"
|
|
115
115
|
private set
|
|
@@ -119,11 +119,11 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
119
119
|
private set
|
|
120
120
|
var networkIsExpensive: Boolean = false
|
|
121
121
|
private set
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
124
124
|
private var netReady = false
|
|
125
125
|
private var live = false
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
private var crashCount = 0
|
|
128
128
|
private var freezeCount = 0
|
|
129
129
|
private var errorCount = 0
|
|
@@ -140,20 +140,21 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
140
140
|
private var hierarchyRunnable: Runnable? = null
|
|
141
141
|
private var lastHierarchyHash: String? = null
|
|
142
142
|
private var durationLimitRunnable: Runnable? = null
|
|
143
|
-
|
|
143
|
+
private val lifecycleContractVersion = 2
|
|
144
|
+
|
|
144
145
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
145
|
-
|
|
146
|
+
|
|
146
147
|
/**
|
|
147
148
|
* Fast session start using existing credentials - skips credential fetch for faster restart
|
|
148
149
|
*/
|
|
149
150
|
fun beginReplayFast(apiToken: String, serverEndpoint: String, credential: String, captureSettings: Map<String, Any>? = null) {
|
|
150
151
|
val perf = PerformanceSnapshot.capture()
|
|
151
152
|
DiagnosticLog.debugSessionCreate("ORCHESTRATOR_FAST_INIT", "beginReplayFast with existing credential", perf)
|
|
152
|
-
|
|
153
|
+
|
|
153
154
|
this.apiToken = apiToken
|
|
154
155
|
this.serverEndpoint = serverEndpoint
|
|
155
156
|
applySettings(captureSettings)
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
// Set credentials AND endpoint directly without network fetch
|
|
158
159
|
TelemetryPipeline.shared?.apiToken = apiToken
|
|
159
160
|
TelemetryPipeline.shared?.credential = credential
|
|
@@ -161,51 +162,51 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
161
162
|
SegmentDispatcher.shared.apiToken = apiToken
|
|
162
163
|
SegmentDispatcher.shared.credential = credential
|
|
163
164
|
SegmentDispatcher.shared.endpoint = serverEndpoint
|
|
164
|
-
|
|
165
|
+
|
|
165
166
|
// Skip network monitoring, assume network is available since we just came from background
|
|
166
167
|
mainHandler.post {
|
|
167
168
|
beginRecording(apiToken)
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
fun beginReplay(apiToken: String, serverEndpoint: String, captureSettings: Map<String, Any>? = null) {
|
|
172
|
-
DiagnosticLog.
|
|
173
|
+
DiagnosticLog.trace("[ReplayOrchestrator] beginReplay v2")
|
|
173
174
|
val perf = PerformanceSnapshot.capture()
|
|
174
175
|
DiagnosticLog.debugSessionCreate("ORCHESTRATOR_INIT", "beginReplay", perf)
|
|
175
|
-
DiagnosticLog.
|
|
176
|
-
|
|
176
|
+
DiagnosticLog.trace("[ReplayOrchestrator] beginReplay called, endpoint=$serverEndpoint")
|
|
177
|
+
|
|
177
178
|
this.apiToken = apiToken
|
|
178
179
|
this.serverEndpoint = serverEndpoint
|
|
179
180
|
applySettings(captureSettings)
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
DiagnosticLog.debugSessionCreate("CREDENTIAL_START", "Requesting device credential")
|
|
182
|
-
DiagnosticLog.
|
|
183
|
-
|
|
183
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Requesting credential from DeviceRegistrar.shared=${DeviceRegistrar.shared != null}")
|
|
184
|
+
|
|
184
185
|
DeviceRegistrar.shared?.obtainCredential(apiToken) { ok, cred ->
|
|
185
|
-
DiagnosticLog.
|
|
186
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Credential callback: ok=$ok, cred=${cred?.take(20) ?: "null"}...")
|
|
186
187
|
if (!ok) {
|
|
187
188
|
DiagnosticLog.debugSessionCreate("CREDENTIAL_FAIL", "Failed")
|
|
188
189
|
DiagnosticLog.caution("[ReplayOrchestrator] Credential fetch FAILED - recording cannot start")
|
|
189
190
|
return@obtainCredential
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
+
|
|
192
193
|
TelemetryPipeline.shared?.apiToken = apiToken
|
|
193
194
|
TelemetryPipeline.shared?.credential = cred
|
|
194
195
|
SegmentDispatcher.shared.apiToken = apiToken
|
|
195
196
|
SegmentDispatcher.shared.credential = cred
|
|
196
|
-
|
|
197
|
-
DiagnosticLog.
|
|
197
|
+
|
|
198
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Credential OK, calling monitorNetwork")
|
|
198
199
|
monitorNetwork(apiToken)
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
+
|
|
202
203
|
fun beginDeferredReplay(apiToken: String, serverEndpoint: String, captureSettings: Map<String, Any>? = null) {
|
|
203
204
|
this.apiToken = apiToken
|
|
204
205
|
this.serverEndpoint = serverEndpoint
|
|
205
206
|
deferredUploadMode = true
|
|
206
|
-
|
|
207
|
+
|
|
207
208
|
applySettings(captureSettings)
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
DeviceRegistrar.shared?.obtainCredential(apiToken) { ok, cred ->
|
|
210
211
|
if (!ok) return@obtainCredential
|
|
211
212
|
TelemetryPipeline.shared?.apiToken = apiToken
|
|
@@ -213,47 +214,55 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
213
214
|
SegmentDispatcher.shared.apiToken = apiToken
|
|
214
215
|
SegmentDispatcher.shared.credential = cred
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
|
|
217
218
|
initSession()
|
|
218
219
|
TelemetryPipeline.shared?.activateDeferredMode()
|
|
219
|
-
|
|
220
|
-
val renderCfg = computeRender(
|
|
221
|
-
|
|
220
|
+
|
|
221
|
+
val renderCfg = computeRender(1, "standard")
|
|
222
|
+
|
|
222
223
|
if (visualCaptureEnabled) {
|
|
223
224
|
VisualCapture.shared?.configure(renderCfg.first, renderCfg.second)
|
|
224
225
|
VisualCapture.shared?.beginCapture(replayStartMs)
|
|
225
226
|
VisualCapture.shared?.activateDeferredMode()
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
if (interactionCaptureEnabled) InteractionRecorder.shared?.activate()
|
|
229
230
|
if (faultTrackingEnabled) StabilityMonitor.shared?.activate()
|
|
230
|
-
|
|
231
|
+
|
|
231
232
|
live = true
|
|
232
233
|
}
|
|
233
|
-
|
|
234
|
+
|
|
234
235
|
fun commitDeferredReplay() {
|
|
235
236
|
deferredUploadMode = false
|
|
236
237
|
TelemetryPipeline.shared?.commitDeferredData()
|
|
237
238
|
VisualCapture.shared?.commitDeferredData()
|
|
238
239
|
TelemetryPipeline.shared?.activate()
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
+
|
|
241
242
|
fun endReplay(completion: ((Boolean, Boolean) -> Unit)? = null) {
|
|
243
|
+
endReplayInternal("unspecified", completion)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun endReplayWithReason(endReason: String, completion: ((Boolean, Boolean) -> Unit)? = null) {
|
|
247
|
+
endReplayInternal(endReason, completion)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private fun endReplayInternal(endReason: String, completion: ((Boolean, Boolean) -> Unit)? = null) {
|
|
242
251
|
if (!live) {
|
|
243
252
|
completion?.invoke(false, false)
|
|
244
253
|
return
|
|
245
254
|
}
|
|
246
255
|
live = false
|
|
247
|
-
|
|
256
|
+
|
|
248
257
|
val sid = replayId ?: ""
|
|
249
258
|
val termMs = System.currentTimeMillis()
|
|
250
259
|
val elapsed = ((termMs - replayStartMs) / 1000).toInt()
|
|
251
|
-
|
|
260
|
+
|
|
252
261
|
unregisterNetworkCallback()
|
|
253
262
|
stopHierarchyCapture()
|
|
254
263
|
stopDurationLimitTimer()
|
|
255
264
|
detachLifecycle()
|
|
256
|
-
|
|
265
|
+
|
|
257
266
|
val metrics = mapOf(
|
|
258
267
|
"crashCount" to crashCount,
|
|
259
268
|
"anrCount" to freezeCount,
|
|
@@ -267,40 +276,50 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
267
276
|
"screensVisited" to visitedScreens.toList(),
|
|
268
277
|
"screenCount" to visitedScreens.toSet().size
|
|
269
278
|
)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
279
|
+
val queueDepthAtFinalize = TelemetryPipeline.shared?.getQueueDepth() ?: 0
|
|
280
|
+
|
|
281
|
+
// Do local teardown immediately so lifecycle rollover never depends on network latency.
|
|
282
|
+
mainHandler.post {
|
|
283
|
+
TelemetryPipeline.shared?.shutdown()
|
|
284
|
+
VisualCapture.shared?.halt()
|
|
285
|
+
InteractionRecorder.shared?.deactivate()
|
|
286
|
+
StabilityMonitor.shared?.deactivate()
|
|
287
|
+
AnrSentinel.shared?.deactivate()
|
|
288
|
+
}
|
|
289
|
+
SegmentDispatcher.shared.shipPending()
|
|
290
|
+
|
|
291
|
+
if (finalized) {
|
|
292
|
+
clearRecovery()
|
|
293
|
+
completion?.invoke(true, true)
|
|
294
|
+
replayId = null
|
|
295
|
+
replayStartMs = 0
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
finalized = true
|
|
299
|
+
|
|
300
|
+
SegmentDispatcher.shared.evaluateReplayRetention(sid, metrics) { _, _ ->
|
|
301
|
+
SegmentDispatcher.shared.concludeReplay(
|
|
302
|
+
sid,
|
|
303
|
+
termMs,
|
|
304
|
+
bgTimeMs,
|
|
305
|
+
metrics,
|
|
306
|
+
queueDepthAtFinalize,
|
|
307
|
+
endReason = endReason,
|
|
308
|
+
lifecycleVersion = lifecycleContractVersion
|
|
309
|
+
) { ok ->
|
|
291
310
|
if (ok) clearRecovery()
|
|
292
311
|
completion?.invoke(true, ok)
|
|
293
312
|
}
|
|
294
313
|
}
|
|
295
|
-
|
|
314
|
+
|
|
296
315
|
replayId = null
|
|
297
316
|
replayStartMs = 0
|
|
298
317
|
}
|
|
299
|
-
|
|
318
|
+
|
|
300
319
|
fun redactView(view: View) {
|
|
301
320
|
VisualCapture.shared?.registerRedaction(view)
|
|
302
321
|
}
|
|
303
|
-
|
|
322
|
+
|
|
304
323
|
/**
|
|
305
324
|
* Set remote configuration from backend
|
|
306
325
|
* Called by JS side before startSession to apply server-side settings
|
|
@@ -315,88 +334,142 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
315
334
|
this.remoteRecordingEnabled = recordingEnabled
|
|
316
335
|
this.remoteSampleRate = sampleRate
|
|
317
336
|
this.remoteMaxRecordingMinutes = maxRecordingMinutes
|
|
318
|
-
|
|
337
|
+
|
|
319
338
|
// Set isSampledIn for server-side enforcement
|
|
320
339
|
// recordingEnabled=false means either dashboard disabled OR session sampled out by JS
|
|
321
340
|
TelemetryPipeline.shared?.isSampledIn = recordingEnabled
|
|
322
|
-
|
|
341
|
+
|
|
323
342
|
// Apply recording settings immediately
|
|
324
343
|
// If recording is disabled, disable visual capture
|
|
325
344
|
if (!recordingEnabled) {
|
|
326
345
|
visualCaptureEnabled = false
|
|
327
|
-
DiagnosticLog.
|
|
346
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Visual capture disabled by remote config (recordingEnabled=false)")
|
|
328
347
|
}
|
|
329
|
-
|
|
348
|
+
|
|
330
349
|
// If already recording, restart the duration limit timer with updated config
|
|
331
350
|
if (live) {
|
|
332
351
|
startDurationLimitTimer()
|
|
333
352
|
}
|
|
334
|
-
|
|
335
|
-
DiagnosticLog.
|
|
353
|
+
|
|
354
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Remote config applied: rejourneyEnabled=$rejourneyEnabled, recordingEnabled=$recordingEnabled, sampleRate=$sampleRate%, maxRecording=${maxRecordingMinutes}min, isSampledIn=$recordingEnabled")
|
|
336
355
|
}
|
|
337
|
-
|
|
356
|
+
|
|
338
357
|
fun unredactView(view: View) {
|
|
339
358
|
VisualCapture.shared?.unregisterRedaction(view)
|
|
340
359
|
}
|
|
341
|
-
|
|
360
|
+
|
|
342
361
|
fun attachAttribute(key: String, value: String) {
|
|
343
362
|
TelemetryPipeline.shared?.recordAttribute(key, value)
|
|
344
363
|
}
|
|
345
|
-
|
|
364
|
+
|
|
346
365
|
fun recordCustomEvent(name: String, payload: String?) {
|
|
347
366
|
TelemetryPipeline.shared?.recordCustomEvent(name, payload ?: "")
|
|
348
367
|
}
|
|
349
|
-
|
|
368
|
+
|
|
350
369
|
fun associateUser(userId: String) {
|
|
351
370
|
TelemetryPipeline.shared?.recordUserAssociation(userId)
|
|
352
371
|
}
|
|
353
|
-
|
|
372
|
+
|
|
354
373
|
fun currentReplayId(): String {
|
|
355
374
|
return replayId ?: ""
|
|
356
375
|
}
|
|
357
|
-
|
|
376
|
+
|
|
358
377
|
fun activateGestureRecording() {
|
|
359
378
|
// Gesture recording activation - handled by InteractionRecorder
|
|
360
379
|
}
|
|
361
|
-
|
|
380
|
+
|
|
362
381
|
fun recoverInterruptedReplay(completion: (String?) -> Unit) {
|
|
363
382
|
val recoveryFile = File(context.filesDir, "rejourney_recovery.json")
|
|
364
|
-
|
|
383
|
+
|
|
365
384
|
if (!recoveryFile.exists()) {
|
|
366
385
|
completion(null)
|
|
367
386
|
return
|
|
368
387
|
}
|
|
369
|
-
|
|
388
|
+
|
|
370
389
|
try {
|
|
371
390
|
val data = recoveryFile.readText()
|
|
372
391
|
val checkpoint = JSONObject(data)
|
|
373
392
|
val recId = checkpoint.optString("replayId", null)
|
|
374
|
-
|
|
393
|
+
|
|
375
394
|
if (recId == null) {
|
|
395
|
+
clearRecovery()
|
|
376
396
|
completion(null)
|
|
377
397
|
return
|
|
378
398
|
}
|
|
379
|
-
|
|
399
|
+
|
|
380
400
|
val origStart = checkpoint.optLong("startMs", 0)
|
|
381
401
|
val nowMs = System.currentTimeMillis()
|
|
382
|
-
|
|
402
|
+
|
|
403
|
+
DiagnosticLog.notice("[ReplayOrchestrator] Recovering interrupted session: $recId")
|
|
404
|
+
|
|
383
405
|
checkpoint.optString("apiToken", null)?.let { SegmentDispatcher.shared.apiToken = it }
|
|
384
406
|
checkpoint.optString("endpoint", null)?.let { SegmentDispatcher.shared.endpoint = it }
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
407
|
+
checkpoint.optString("credential", null)?.let { SegmentDispatcher.shared.credential = it }
|
|
408
|
+
SegmentDispatcher.shared.currentReplayId = recId
|
|
409
|
+
SegmentDispatcher.shared.activate()
|
|
410
|
+
TelemetryPipeline.shared?.currentReplayId = recId
|
|
411
|
+
val hasCrashIncident = hasStoredCrashIncidentForSession(recId)
|
|
412
|
+
|
|
413
|
+
val finalizeRecoveredSession = {
|
|
414
|
+
val crashMetrics = mapOf(
|
|
415
|
+
"crashCount" to if (hasCrashIncident) 1 else 0,
|
|
416
|
+
"durationSeconds" to ((nowMs - origStart) / 1000).toInt()
|
|
417
|
+
)
|
|
418
|
+
val queueDepthAtFinalize = TelemetryPipeline.shared?.getQueueDepth() ?: 0
|
|
419
|
+
|
|
420
|
+
SegmentDispatcher.shared.concludeReplay(
|
|
421
|
+
recId,
|
|
422
|
+
nowMs,
|
|
423
|
+
0,
|
|
424
|
+
crashMetrics,
|
|
425
|
+
queueDepthAtFinalize,
|
|
426
|
+
endReason = "recovery_finalize",
|
|
427
|
+
lifecycleVersion = lifecycleContractVersion
|
|
428
|
+
) { ok ->
|
|
429
|
+
DiagnosticLog.notice("[ReplayOrchestrator] Crash recovery finalize: success=$ok, sessionId=$recId")
|
|
430
|
+
if (ok) {
|
|
431
|
+
clearRecovery()
|
|
432
|
+
}
|
|
433
|
+
completion(if (ok) recId else null)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
val visualCapture = VisualCapture.shared
|
|
438
|
+
if (visualCapture == null) {
|
|
439
|
+
finalizeRecoveredSession()
|
|
440
|
+
} else {
|
|
441
|
+
visualCapture.uploadPendingFrames(recId) { framesUploaded ->
|
|
442
|
+
if (!framesUploaded) {
|
|
443
|
+
DiagnosticLog.caution("[ReplayOrchestrator] Crash recovery postponed: pending frame upload failed for session $recId")
|
|
444
|
+
completion(null)
|
|
445
|
+
return@uploadPendingFrames
|
|
446
|
+
}
|
|
447
|
+
finalizeRecoveredSession()
|
|
448
|
+
}
|
|
394
449
|
}
|
|
395
450
|
} catch (e: Exception) {
|
|
451
|
+
DiagnosticLog.fault("[ReplayOrchestrator] Crash recovery failed: ${e.message}")
|
|
396
452
|
completion(null)
|
|
397
453
|
}
|
|
398
454
|
}
|
|
399
|
-
|
|
455
|
+
|
|
456
|
+
private fun hasStoredCrashIncidentForSession(sessionId: String): Boolean {
|
|
457
|
+
val incidentFile = File(context.cacheDir, "rj_incidents.json")
|
|
458
|
+
if (!incidentFile.exists()) return false
|
|
459
|
+
|
|
460
|
+
return try {
|
|
461
|
+
val incident = JSONObject(incidentFile.readText())
|
|
462
|
+
val incidentSessionId = incident.optString("sessionId", "")
|
|
463
|
+
val category = incident.optString("category", "").lowercase()
|
|
464
|
+
val crashLikeCategory = category == "signal" || category == "exception" || category == "crash"
|
|
465
|
+
val hasSignalDetail = incident.optString("identifier", "").isNotBlank()
|
|
466
|
+
|| incident.optString("detail", "").isNotBlank()
|
|
467
|
+
crashLikeCategory && incidentSessionId == sessionId && hasSignalDetail
|
|
468
|
+
} catch (_: Exception) {
|
|
469
|
+
false
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
400
473
|
// Tally methods
|
|
401
474
|
fun incrementFaultTally() { crashCount++ }
|
|
402
475
|
fun incrementStalledTally() { freezeCount++ }
|
|
@@ -406,21 +479,21 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
406
479
|
fun incrementGestureTally() { gestureCount++ }
|
|
407
480
|
fun incrementRageTapTally() { rageCount++ }
|
|
408
481
|
fun incrementDeadTapTally() { deadTapCount++ }
|
|
409
|
-
|
|
482
|
+
|
|
410
483
|
fun logScreenView(screenId: String) {
|
|
411
484
|
if (screenId.isEmpty()) return
|
|
412
485
|
visitedScreens.add(screenId)
|
|
413
486
|
currentScreenName = screenId
|
|
414
487
|
if (hierarchyCaptureEnabled) captureHierarchy()
|
|
415
488
|
}
|
|
416
|
-
|
|
489
|
+
|
|
417
490
|
private fun initSession() {
|
|
418
491
|
replayStartMs = System.currentTimeMillis()
|
|
419
492
|
// Always generate a fresh session ID - never reuse stale IDs
|
|
420
493
|
val uuidPart = UUID.randomUUID().toString().replace("-", "").lowercase()
|
|
421
494
|
replayId = "session_${replayStartMs}_$uuidPart"
|
|
422
495
|
finalized = false
|
|
423
|
-
|
|
496
|
+
|
|
424
497
|
crashCount = 0
|
|
425
498
|
freezeCount = 0
|
|
426
499
|
errorCount = 0
|
|
@@ -432,27 +505,27 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
432
505
|
visitedScreens.clear()
|
|
433
506
|
bgTimeMs = 0
|
|
434
507
|
bgStartMs = null
|
|
435
|
-
|
|
508
|
+
|
|
436
509
|
TelemetryPipeline.shared?.currentReplayId = replayId
|
|
437
510
|
SegmentDispatcher.shared.currentReplayId = replayId
|
|
438
511
|
StabilityMonitor.shared?.currentSessionId = replayId
|
|
439
|
-
|
|
512
|
+
|
|
440
513
|
attachLifecycle()
|
|
441
514
|
saveRecovery()
|
|
442
|
-
|
|
515
|
+
|
|
443
516
|
recordAppStartup()
|
|
444
517
|
}
|
|
445
|
-
|
|
518
|
+
|
|
446
519
|
private fun recordAppStartup() {
|
|
447
520
|
val nowMs = System.currentTimeMillis()
|
|
448
521
|
val startupDurationMs = nowMs - processStartTime
|
|
449
|
-
|
|
522
|
+
|
|
450
523
|
// Only record if it's a reasonable startup time (> 0 and < 60 seconds)
|
|
451
524
|
if (startupDurationMs > 0 && startupDurationMs < 60000) {
|
|
452
525
|
TelemetryPipeline.shared?.recordAppStartup(startupDurationMs)
|
|
453
526
|
}
|
|
454
527
|
}
|
|
455
|
-
|
|
528
|
+
|
|
456
529
|
private fun applySettings(cfg: Map<String, Any>?) {
|
|
457
530
|
if (cfg == null) return
|
|
458
531
|
snapshotInterval = (cfg["captureRate"] as? Double) ?: 0.33
|
|
@@ -465,43 +538,43 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
465
538
|
wifiRequired = (cfg["wifiOnly"] as? Boolean) ?: false
|
|
466
539
|
frameBundleSize = (cfg["screenshotBatchSize"] as? Int) ?: 5
|
|
467
540
|
}
|
|
468
|
-
|
|
541
|
+
|
|
469
542
|
private fun monitorNetwork(token: String) {
|
|
470
|
-
DiagnosticLog.
|
|
543
|
+
DiagnosticLog.trace("[ReplayOrchestrator] monitorNetwork called")
|
|
471
544
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
|
472
545
|
if (connectivityManager == null) {
|
|
473
|
-
DiagnosticLog.
|
|
546
|
+
DiagnosticLog.trace("[ReplayOrchestrator] No ConnectivityManager, starting recording directly")
|
|
474
547
|
beginRecording(token)
|
|
475
548
|
return
|
|
476
549
|
}
|
|
477
|
-
|
|
550
|
+
|
|
478
551
|
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
479
552
|
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
|
|
480
553
|
handleNetworkChange(capabilities, token)
|
|
481
554
|
}
|
|
482
|
-
|
|
555
|
+
|
|
483
556
|
override fun onLost(network: Network) {
|
|
484
557
|
currentNetworkType = "none"
|
|
485
558
|
netReady = false
|
|
486
559
|
}
|
|
487
560
|
}
|
|
488
|
-
|
|
561
|
+
|
|
489
562
|
val request = NetworkRequest.Builder()
|
|
490
563
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
491
564
|
.build()
|
|
492
|
-
|
|
565
|
+
|
|
493
566
|
try {
|
|
494
567
|
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
|
495
|
-
|
|
568
|
+
|
|
496
569
|
// Check current network state immediately (callback only fires on CHANGES)
|
|
497
570
|
val activeNetwork = connectivityManager.activeNetwork
|
|
498
571
|
val capabilities = activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
|
|
499
|
-
DiagnosticLog.
|
|
572
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Network check: activeNetwork=${activeNetwork != null}, capabilities=${capabilities != null}")
|
|
500
573
|
if (capabilities != null) {
|
|
501
574
|
handleNetworkChange(capabilities, token)
|
|
502
575
|
} else {
|
|
503
576
|
// No active network - start recording anyway, uploads will retry when network available
|
|
504
|
-
DiagnosticLog.
|
|
577
|
+
DiagnosticLog.trace("[ReplayOrchestrator] No active network, starting recording anyway")
|
|
505
578
|
mainHandler.post { beginRecording(token) }
|
|
506
579
|
}
|
|
507
580
|
} catch (e: Exception) {
|
|
@@ -509,31 +582,31 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
509
582
|
beginRecording(token)
|
|
510
583
|
}
|
|
511
584
|
}
|
|
512
|
-
|
|
585
|
+
|
|
513
586
|
private fun handleNetworkChange(capabilities: NetworkCapabilities, token: String) {
|
|
514
587
|
val isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
515
588
|
val isCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
516
589
|
val isEthernet = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
|
517
|
-
|
|
590
|
+
|
|
518
591
|
networkIsExpensive = !isWifi && !isEthernet
|
|
519
592
|
networkIsConstrained = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
520
593
|
!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
|
521
594
|
} else {
|
|
522
595
|
networkIsExpensive
|
|
523
596
|
}
|
|
524
|
-
|
|
597
|
+
|
|
525
598
|
currentNetworkType = when {
|
|
526
599
|
isWifi -> "wifi"
|
|
527
600
|
isCellular -> "cellular"
|
|
528
601
|
isEthernet -> "wired"
|
|
529
602
|
else -> "other"
|
|
530
603
|
}
|
|
531
|
-
|
|
604
|
+
|
|
532
605
|
val canProceed = when {
|
|
533
606
|
wifiRequired && !isWifi -> false
|
|
534
607
|
else -> true
|
|
535
608
|
}
|
|
536
|
-
|
|
609
|
+
|
|
537
610
|
mainHandler.post {
|
|
538
611
|
netReady = canProceed
|
|
539
612
|
if (canProceed && !live) {
|
|
@@ -541,108 +614,109 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
541
614
|
}
|
|
542
615
|
}
|
|
543
616
|
}
|
|
544
|
-
|
|
617
|
+
|
|
545
618
|
private fun beginRecording(token: String) {
|
|
546
|
-
DiagnosticLog.
|
|
619
|
+
DiagnosticLog.trace("[ReplayOrchestrator] beginRecording called, live=$live")
|
|
547
620
|
if (live) {
|
|
548
|
-
DiagnosticLog.
|
|
621
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Already live, skipping")
|
|
549
622
|
return
|
|
550
623
|
}
|
|
551
624
|
live = true
|
|
552
|
-
|
|
625
|
+
|
|
553
626
|
this.apiToken = token
|
|
554
627
|
initSession()
|
|
555
|
-
DiagnosticLog.
|
|
556
|
-
|
|
628
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Session initialized: replayId=$replayId")
|
|
629
|
+
|
|
557
630
|
// Reactivate the dispatcher in case it was halted from a previous session
|
|
558
631
|
SegmentDispatcher.shared.activate()
|
|
559
632
|
TelemetryPipeline.shared?.activate()
|
|
560
|
-
|
|
561
|
-
val renderCfg = computeRender(
|
|
562
|
-
DiagnosticLog.
|
|
633
|
+
|
|
634
|
+
val renderCfg = computeRender(1, "standard")
|
|
635
|
+
DiagnosticLog.trace("[ReplayOrchestrator] VisualCapture.shared=${VisualCapture.shared != null}, visualCaptureEnabled=$visualCaptureEnabled")
|
|
563
636
|
VisualCapture.shared?.configure(renderCfg.first, renderCfg.second)
|
|
564
|
-
|
|
637
|
+
|
|
565
638
|
if (visualCaptureEnabled) {
|
|
566
|
-
DiagnosticLog.
|
|
639
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Starting VisualCapture")
|
|
567
640
|
VisualCapture.shared?.beginCapture(replayStartMs)
|
|
568
641
|
}
|
|
569
642
|
if (interactionCaptureEnabled) InteractionRecorder.shared?.activate()
|
|
570
643
|
if (faultTrackingEnabled) StabilityMonitor.shared?.activate()
|
|
571
644
|
if (responsivenessCaptureEnabled) AnrSentinel.shared?.activate()
|
|
572
645
|
if (hierarchyCaptureEnabled) startHierarchyCapture()
|
|
573
|
-
|
|
646
|
+
|
|
574
647
|
// Start duration limit timer based on remote config
|
|
575
648
|
startDurationLimitTimer()
|
|
576
|
-
|
|
577
|
-
DiagnosticLog.
|
|
649
|
+
|
|
650
|
+
DiagnosticLog.trace("[ReplayOrchestrator] beginRecording completed")
|
|
578
651
|
}
|
|
579
|
-
|
|
652
|
+
|
|
580
653
|
// MARK: - Duration Limit Timer
|
|
581
|
-
|
|
654
|
+
|
|
582
655
|
private fun startDurationLimitTimer() {
|
|
583
656
|
stopDurationLimitTimer()
|
|
584
|
-
|
|
657
|
+
|
|
585
658
|
val maxMinutes = remoteMaxRecordingMinutes
|
|
586
659
|
if (maxMinutes <= 0) return
|
|
587
|
-
|
|
660
|
+
|
|
588
661
|
val maxMs = maxMinutes.toLong() * 60 * 1000
|
|
589
662
|
val now = System.currentTimeMillis()
|
|
590
663
|
val elapsed = now - replayStartMs
|
|
591
664
|
val remaining = if (maxMs > elapsed) maxMs - elapsed else 0L
|
|
592
|
-
|
|
665
|
+
|
|
593
666
|
if (remaining <= 0) {
|
|
594
|
-
DiagnosticLog.
|
|
595
|
-
|
|
667
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Duration limit already exceeded, stopping session")
|
|
668
|
+
endReplayWithReason("duration_limit")
|
|
596
669
|
return
|
|
597
670
|
}
|
|
598
|
-
|
|
671
|
+
|
|
599
672
|
durationLimitRunnable = Runnable {
|
|
600
673
|
if (!live) return@Runnable
|
|
601
|
-
DiagnosticLog.
|
|
602
|
-
|
|
674
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Recording duration limit reached (${maxMinutes}min), stopping session")
|
|
675
|
+
endReplayWithReason("duration_limit")
|
|
603
676
|
}
|
|
604
677
|
mainHandler.postDelayed(durationLimitRunnable!!, remaining)
|
|
605
|
-
|
|
606
|
-
DiagnosticLog.
|
|
678
|
+
|
|
679
|
+
DiagnosticLog.trace("[ReplayOrchestrator] Duration limit timer set: ${remaining / 1000}s remaining (max ${maxMinutes}min)")
|
|
607
680
|
}
|
|
608
|
-
|
|
681
|
+
|
|
609
682
|
private fun stopDurationLimitTimer() {
|
|
610
683
|
durationLimitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
611
684
|
durationLimitRunnable = null
|
|
612
685
|
}
|
|
613
|
-
|
|
686
|
+
|
|
614
687
|
private fun saveRecovery() {
|
|
615
688
|
val sid = replayId ?: return
|
|
616
689
|
val token = apiToken ?: return
|
|
617
|
-
|
|
690
|
+
|
|
618
691
|
val checkpoint = JSONObject().apply {
|
|
619
692
|
put("replayId", sid)
|
|
620
693
|
put("apiToken", token)
|
|
621
694
|
put("startMs", replayStartMs)
|
|
622
695
|
put("endpoint", serverEndpoint)
|
|
696
|
+
SegmentDispatcher.shared.credential?.let { put("credential", it) }
|
|
623
697
|
}
|
|
624
|
-
|
|
698
|
+
|
|
625
699
|
try {
|
|
626
700
|
File(context.filesDir, "rejourney_recovery.json").writeText(checkpoint.toString())
|
|
627
701
|
} catch (_: Exception) { }
|
|
628
702
|
}
|
|
629
|
-
|
|
703
|
+
|
|
630
704
|
private fun clearRecovery() {
|
|
631
705
|
try {
|
|
632
706
|
File(context.filesDir, "rejourney_recovery.json").delete()
|
|
633
707
|
} catch (_: Exception) { }
|
|
634
708
|
}
|
|
635
|
-
|
|
709
|
+
|
|
636
710
|
private fun attachLifecycle() {
|
|
637
711
|
val app = context as? Application ?: return
|
|
638
712
|
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
|
639
713
|
}
|
|
640
|
-
|
|
714
|
+
|
|
641
715
|
private fun detachLifecycle() {
|
|
642
716
|
val app = context as? Application ?: return
|
|
643
717
|
app.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
|
|
644
718
|
}
|
|
645
|
-
|
|
719
|
+
|
|
646
720
|
private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
|
|
647
721
|
override fun onActivityResumed(activity: Activity) {
|
|
648
722
|
bgStartMs?.let { start ->
|
|
@@ -650,19 +724,24 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
650
724
|
bgTimeMs += (now - start)
|
|
651
725
|
}
|
|
652
726
|
bgStartMs = null
|
|
727
|
+
|
|
728
|
+
if (responsivenessCaptureEnabled) {
|
|
729
|
+
AnrSentinel.shared.activate()
|
|
730
|
+
}
|
|
653
731
|
}
|
|
654
|
-
|
|
732
|
+
|
|
655
733
|
override fun onActivityPaused(activity: Activity) {
|
|
656
734
|
bgStartMs = System.currentTimeMillis()
|
|
735
|
+
AnrSentinel.shared.deactivate()
|
|
657
736
|
}
|
|
658
|
-
|
|
737
|
+
|
|
659
738
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
|
660
739
|
override fun onActivityStarted(activity: Activity) {}
|
|
661
740
|
override fun onActivityStopped(activity: Activity) {}
|
|
662
741
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
|
663
742
|
override fun onActivityDestroyed(activity: Activity) {}
|
|
664
743
|
}
|
|
665
|
-
|
|
744
|
+
|
|
666
745
|
private fun unregisterNetworkCallback() {
|
|
667
746
|
networkCallback?.let { callback ->
|
|
668
747
|
try {
|
|
@@ -672,10 +751,10 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
672
751
|
}
|
|
673
752
|
networkCallback = null
|
|
674
753
|
}
|
|
675
|
-
|
|
754
|
+
|
|
676
755
|
private fun startHierarchyCapture() {
|
|
677
756
|
stopHierarchyCapture()
|
|
678
|
-
|
|
757
|
+
|
|
679
758
|
hierarchyHandler = Handler(Looper.getMainLooper())
|
|
680
759
|
hierarchyRunnable = object : Runnable {
|
|
681
760
|
override fun run() {
|
|
@@ -684,38 +763,45 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
684
763
|
}
|
|
685
764
|
}
|
|
686
765
|
hierarchyHandler?.postDelayed(hierarchyRunnable!!, (hierarchyCaptureInterval * 1000).toLong())
|
|
687
|
-
|
|
766
|
+
|
|
688
767
|
// Initial capture after 500ms
|
|
689
768
|
hierarchyHandler?.postDelayed({ captureHierarchy() }, 500)
|
|
690
769
|
}
|
|
691
|
-
|
|
770
|
+
|
|
692
771
|
private fun stopHierarchyCapture() {
|
|
693
772
|
hierarchyRunnable?.let { hierarchyHandler?.removeCallbacks(it) }
|
|
694
773
|
hierarchyHandler = null
|
|
695
774
|
hierarchyRunnable = null
|
|
696
775
|
}
|
|
697
|
-
|
|
776
|
+
|
|
698
777
|
private fun captureHierarchy() {
|
|
699
778
|
if (!live) return
|
|
700
779
|
val sid = replayId ?: return
|
|
701
|
-
|
|
780
|
+
|
|
702
781
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
703
782
|
mainHandler.post { captureHierarchy() }
|
|
704
783
|
return
|
|
705
784
|
}
|
|
706
|
-
|
|
785
|
+
|
|
786
|
+
// Throttle hierarchy capture when map is visible and animating —
|
|
787
|
+
// ViewHierarchyScanner traverses the full view tree including map's
|
|
788
|
+
// deep SurfaceView/TextureView children, adding main-thread pressure.
|
|
789
|
+
if (SpecialCases.shared.mapVisible && !SpecialCases.shared.mapIdle) {
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
707
793
|
val hierarchy = ViewHierarchyScanner.shared?.captureHierarchy() ?: return
|
|
708
|
-
|
|
794
|
+
|
|
709
795
|
val hash = hierarchyHash(hierarchy)
|
|
710
796
|
if (hash == lastHierarchyHash) return
|
|
711
797
|
lastHierarchyHash = hash
|
|
712
|
-
|
|
798
|
+
|
|
713
799
|
val json = JSONObject(hierarchy).toString().toByteArray(Charsets.UTF_8)
|
|
714
800
|
val ts = System.currentTimeMillis()
|
|
715
|
-
|
|
801
|
+
|
|
716
802
|
SegmentDispatcher.shared.transmitHierarchy(sid, json, ts, null)
|
|
717
803
|
}
|
|
718
|
-
|
|
804
|
+
|
|
719
805
|
private fun hierarchyHash(h: Map<String, Any>): String {
|
|
720
806
|
val screen = currentScreenName ?: "unknown"
|
|
721
807
|
var childCount = 0
|
|
@@ -729,12 +815,12 @@ class ReplayOrchestrator private constructor(private val context: Context) {
|
|
|
729
815
|
}
|
|
730
816
|
|
|
731
817
|
private fun computeRender(fps: Int, tier: String): Pair<Double, Double> {
|
|
732
|
-
val
|
|
733
|
-
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
818
|
+
val tierLower = tier.lowercase()
|
|
819
|
+
return when (tierLower) {
|
|
820
|
+
"minimal" -> Pair(2.0, 0.4) // 0.5 fps for maximum size reduction
|
|
821
|
+
"low" -> Pair(1.0 / fps.coerceIn(1, 99), 0.4)
|
|
822
|
+
"standard" -> Pair(1.0 / fps.coerceIn(1, 99), 0.5)
|
|
823
|
+
"high" -> Pair(1.0 / fps.coerceIn(1, 99), 0.55)
|
|
824
|
+
else -> Pair(1.0 / fps.coerceIn(1, 99), 0.5)
|
|
825
|
+
}
|
|
740
826
|
}
|