@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
|
@@ -58,11 +58,67 @@ class SegmentDispatcher private constructor() {
|
|
|
58
58
|
private val circuitBreakerThreshold = 5
|
|
59
59
|
private val circuitResetTime: Long = 60_000 // 60 seconds
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
var
|
|
64
|
-
var
|
|
65
|
-
var
|
|
61
|
+
// Per-session SDK telemetry counters
|
|
62
|
+
private val metricsLock = ReentrantLock()
|
|
63
|
+
private var _uploadSuccessCount = 0
|
|
64
|
+
private var _uploadFailureCount = 0
|
|
65
|
+
private var _retryAttemptCount = 0
|
|
66
|
+
private var _circuitBreakerOpenCount = 0
|
|
67
|
+
private var _memoryEvictionCount = 0
|
|
68
|
+
private var _offlinePersistCount = 0
|
|
69
|
+
private var _sessionStartCount = 0
|
|
70
|
+
private var _crashCount = 0
|
|
71
|
+
private var _totalBytesUploaded = 0L
|
|
72
|
+
private var _totalBytesEvicted = 0L
|
|
73
|
+
private var _totalUploadDurationMs = 0.0
|
|
74
|
+
private var _uploadDurationSampleCount = 0
|
|
75
|
+
private var _lastUploadTime: Long? = null
|
|
76
|
+
private var _lastRetryTime: Long? = null
|
|
77
|
+
|
|
78
|
+
val uploadSuccessCount: Int
|
|
79
|
+
get() = metricsLock.withLock { _uploadSuccessCount }
|
|
80
|
+
|
|
81
|
+
val uploadFailureCount: Int
|
|
82
|
+
get() = metricsLock.withLock { _uploadFailureCount }
|
|
83
|
+
|
|
84
|
+
val retryAttemptCount: Int
|
|
85
|
+
get() = metricsLock.withLock { _retryAttemptCount }
|
|
86
|
+
|
|
87
|
+
val circuitBreakerOpenCount: Int
|
|
88
|
+
get() = metricsLock.withLock { _circuitBreakerOpenCount }
|
|
89
|
+
|
|
90
|
+
val memoryEvictionCount: Int
|
|
91
|
+
get() = metricsLock.withLock { _memoryEvictionCount }
|
|
92
|
+
|
|
93
|
+
val offlinePersistCount: Int
|
|
94
|
+
get() = metricsLock.withLock { _offlinePersistCount }
|
|
95
|
+
|
|
96
|
+
val sessionStartCount: Int
|
|
97
|
+
get() = metricsLock.withLock { _sessionStartCount }
|
|
98
|
+
|
|
99
|
+
val crashCount: Int
|
|
100
|
+
get() = metricsLock.withLock { _crashCount }
|
|
101
|
+
|
|
102
|
+
val avgUploadDurationMs: Double
|
|
103
|
+
get() = metricsLock.withLock {
|
|
104
|
+
if (_uploadDurationSampleCount > 0) {
|
|
105
|
+
_totalUploadDurationMs / _uploadDurationSampleCount.toDouble()
|
|
106
|
+
} else {
|
|
107
|
+
0.0
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
val lastUploadTime: Long?
|
|
112
|
+
get() = metricsLock.withLock { _lastUploadTime }
|
|
113
|
+
|
|
114
|
+
val lastRetryTime: Long?
|
|
115
|
+
get() = metricsLock.withLock { _lastRetryTime }
|
|
116
|
+
|
|
117
|
+
val totalBytesUploaded: Long
|
|
118
|
+
get() = metricsLock.withLock { _totalBytesUploaded }
|
|
119
|
+
|
|
120
|
+
val totalBytesEvicted: Long
|
|
121
|
+
get() = metricsLock.withLock { _totalBytesEvicted }
|
|
66
122
|
|
|
67
123
|
private val workerExecutor = Executors.newFixedThreadPool(2)
|
|
68
124
|
private val scope = CoroutineScope(workerExecutor.asCoroutineDispatcher() + SupervisorJob())
|
|
@@ -86,6 +142,7 @@ class SegmentDispatcher private constructor() {
|
|
|
86
142
|
batchSeqNumber = 0
|
|
87
143
|
billingBlocked = false
|
|
88
144
|
consecutiveFailures = 0
|
|
145
|
+
resetSessionTelemetry()
|
|
89
146
|
}
|
|
90
147
|
|
|
91
148
|
fun activate() {
|
|
@@ -113,14 +170,14 @@ class SegmentDispatcher private constructor() {
|
|
|
113
170
|
) {
|
|
114
171
|
val sid = currentReplayId
|
|
115
172
|
val canUpload = canUploadNow()
|
|
116
|
-
DiagnosticLog.
|
|
173
|
+
DiagnosticLog.trace("[SegmentDispatcher] transmitFrameBundle: sid=${sid?.take(12) ?: "null"}, canUpload=$canUpload, frames=$frameCount, bytes=${payload.size}")
|
|
117
174
|
|
|
118
175
|
if (sid != null) {
|
|
119
176
|
DiagnosticLog.debugPresignRequest(endpoint, sid, "screenshots", payload.size)
|
|
120
177
|
}
|
|
121
178
|
|
|
122
179
|
if (sid == null || !canUpload) {
|
|
123
|
-
DiagnosticLog.
|
|
180
|
+
DiagnosticLog.trace("[SegmentDispatcher] transmitFrameBundle: rejected - sid=${sid != null}, canUpload=$canUpload")
|
|
124
181
|
completion?.invoke(false)
|
|
125
182
|
return
|
|
126
183
|
}
|
|
@@ -201,15 +258,22 @@ class SegmentDispatcher private constructor() {
|
|
|
201
258
|
concludedAt: Long,
|
|
202
259
|
backgroundDurationMs: Long,
|
|
203
260
|
metrics: Map<String, Any>?,
|
|
261
|
+
currentQueueDepth: Int = 0,
|
|
262
|
+
endReason: String? = null,
|
|
263
|
+
lifecycleVersion: Int? = null,
|
|
204
264
|
completion: (Boolean) -> Unit
|
|
205
265
|
) {
|
|
206
266
|
val url = "$endpoint/api/ingest/session/end"
|
|
267
|
+
ingestFinalizeMetrics(metrics)
|
|
207
268
|
|
|
208
269
|
val body = JSONObject().apply {
|
|
209
270
|
put("sessionId", replayId)
|
|
210
271
|
put("endedAt", concludedAt)
|
|
211
272
|
if (backgroundDurationMs > 0) put("totalBackgroundTimeMs", backgroundDurationMs)
|
|
212
273
|
metrics?.let { put("metrics", JSONObject(it)) }
|
|
274
|
+
put("sdkTelemetry", buildSdkTelemetry(currentQueueDepth))
|
|
275
|
+
if (!endReason.isNullOrBlank()) put("endReason", endReason)
|
|
276
|
+
if ((lifecycleVersion ?: 0) > 0) put("lifecycleVersion", lifecycleVersion)
|
|
213
277
|
}
|
|
214
278
|
|
|
215
279
|
val request = buildRequest(url, body)
|
|
@@ -257,6 +321,7 @@ class SegmentDispatcher private constructor() {
|
|
|
257
321
|
}
|
|
258
322
|
}
|
|
259
323
|
|
|
324
|
+
@Synchronized
|
|
260
325
|
private fun canUploadNow(): Boolean {
|
|
261
326
|
if (billingBlocked) return false
|
|
262
327
|
if (circuitOpen) {
|
|
@@ -269,25 +334,36 @@ class SegmentDispatcher private constructor() {
|
|
|
269
334
|
return true
|
|
270
335
|
}
|
|
271
336
|
|
|
337
|
+
@Synchronized
|
|
272
338
|
private fun registerFailure() {
|
|
273
339
|
consecutiveFailures++
|
|
274
|
-
|
|
340
|
+
metricsLock.withLock {
|
|
341
|
+
_uploadFailureCount++
|
|
342
|
+
}
|
|
275
343
|
if (consecutiveFailures >= circuitBreakerThreshold) {
|
|
276
|
-
if (!circuitOpen)
|
|
344
|
+
if (!circuitOpen) {
|
|
345
|
+
metricsLock.withLock {
|
|
346
|
+
_circuitBreakerOpenCount++
|
|
347
|
+
}
|
|
348
|
+
}
|
|
277
349
|
circuitOpen = true
|
|
278
350
|
circuitOpenTime = System.currentTimeMillis()
|
|
279
351
|
}
|
|
280
352
|
}
|
|
281
353
|
|
|
354
|
+
@Synchronized
|
|
282
355
|
private fun registerSuccess() {
|
|
283
356
|
consecutiveFailures = 0
|
|
284
|
-
|
|
357
|
+
metricsLock.withLock {
|
|
358
|
+
_uploadSuccessCount++
|
|
359
|
+
_lastUploadTime = System.currentTimeMillis()
|
|
360
|
+
}
|
|
285
361
|
}
|
|
286
362
|
|
|
287
363
|
private fun scheduleUpload(upload: PendingUpload, completion: ((Boolean) -> Unit)?) {
|
|
288
|
-
DiagnosticLog.
|
|
364
|
+
DiagnosticLog.trace("[SegmentDispatcher] scheduleUpload: active=$active, type=${upload.contentType}, items=${upload.itemCount}")
|
|
289
365
|
if (!active) {
|
|
290
|
-
DiagnosticLog.
|
|
366
|
+
DiagnosticLog.trace("[SegmentDispatcher] scheduleUpload: rejected - not active")
|
|
291
367
|
completion?.invoke(false)
|
|
292
368
|
return
|
|
293
369
|
}
|
|
@@ -304,16 +380,14 @@ class SegmentDispatcher private constructor() {
|
|
|
304
380
|
|
|
305
381
|
val presignResponse = requestPresignedUrl(upload)
|
|
306
382
|
if (presignResponse == null) {
|
|
307
|
-
DiagnosticLog.notice("[SegmentDispatcher] ❌ requestPresignedUrl FAILED for ${upload.contentType}")
|
|
308
383
|
DiagnosticLog.caution("[SegmentDispatcher] requestPresignedUrl FAILED for ${upload.contentType}")
|
|
309
384
|
registerFailure()
|
|
310
385
|
scheduleRetryIfNeeded(upload, completion)
|
|
311
386
|
return
|
|
312
387
|
}
|
|
313
388
|
|
|
314
|
-
val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload
|
|
389
|
+
val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload)
|
|
315
390
|
if (!s3ok) {
|
|
316
|
-
DiagnosticLog.notice("[SegmentDispatcher] ❌ uploadToS3 FAILED for ${upload.contentType}")
|
|
317
391
|
DiagnosticLog.caution("[SegmentDispatcher] uploadToS3 FAILED for ${upload.contentType}")
|
|
318
392
|
registerFailure()
|
|
319
393
|
scheduleRetryIfNeeded(upload, completion)
|
|
@@ -324,7 +398,6 @@ class SegmentDispatcher private constructor() {
|
|
|
324
398
|
if (confirmOk) {
|
|
325
399
|
registerSuccess()
|
|
326
400
|
} else {
|
|
327
|
-
DiagnosticLog.notice("[SegmentDispatcher] ❌ confirmBatchComplete FAILED for ${upload.contentType}")
|
|
328
401
|
DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType}")
|
|
329
402
|
registerFailure()
|
|
330
403
|
}
|
|
@@ -337,6 +410,10 @@ class SegmentDispatcher private constructor() {
|
|
|
337
410
|
retryLock.withLock {
|
|
338
411
|
retryQueue.add(retry)
|
|
339
412
|
}
|
|
413
|
+
metricsLock.withLock {
|
|
414
|
+
_retryAttemptCount++
|
|
415
|
+
_lastRetryTime = System.currentTimeMillis()
|
|
416
|
+
}
|
|
340
417
|
}
|
|
341
418
|
completion?.invoke(false)
|
|
342
419
|
}
|
|
@@ -362,7 +439,7 @@ class SegmentDispatcher private constructor() {
|
|
|
362
439
|
|
|
363
440
|
if (upload.contentType == "events") {
|
|
364
441
|
put("contentType", "events")
|
|
365
|
-
put("batchNumber",
|
|
442
|
+
put("batchNumber", upload.batchNumber)
|
|
366
443
|
put("isSampledIn", isSampledIn) // Server-side enforcement
|
|
367
444
|
} else {
|
|
368
445
|
put("kind", upload.contentType)
|
|
@@ -384,14 +461,14 @@ class SegmentDispatcher private constructor() {
|
|
|
384
461
|
DiagnosticLog.debugPresignResponse(response.code, null, null, durationMs)
|
|
385
462
|
|
|
386
463
|
if (response.code == 402) {
|
|
387
|
-
DiagnosticLog.
|
|
464
|
+
DiagnosticLog.caution("[SegmentDispatcher] presign: 402 Payment Required - billing blocked")
|
|
388
465
|
billingBlocked = true
|
|
389
466
|
return null
|
|
390
467
|
}
|
|
391
468
|
|
|
392
469
|
if (response.code != 200 || responseBody == null) {
|
|
393
470
|
val bodyPreview = responseBody?.take(300) ?: "null"
|
|
394
|
-
DiagnosticLog.
|
|
471
|
+
DiagnosticLog.caution("[SegmentDispatcher] presign failed: status=${response.code} body=$bodyPreview")
|
|
395
472
|
return null
|
|
396
473
|
}
|
|
397
474
|
|
|
@@ -410,17 +487,14 @@ class SegmentDispatcher private constructor() {
|
|
|
410
487
|
PresignResponse(presignedUrl, batchId)
|
|
411
488
|
} catch (e: Exception) {
|
|
412
489
|
val durationMs = (System.currentTimeMillis() - startTime).toDouble()
|
|
413
|
-
DiagnosticLog.
|
|
490
|
+
DiagnosticLog.trace("[SegmentDispatcher] presign exception (${durationMs.toLong()}ms): ${e.javaClass.simpleName}: ${e.message}")
|
|
414
491
|
DiagnosticLog.fault("[SegmentDispatcher] presign exception: ${e.message}")
|
|
415
492
|
null
|
|
416
493
|
}
|
|
417
494
|
}
|
|
418
495
|
|
|
419
|
-
private suspend fun uploadToS3(url: String, payload: ByteArray
|
|
420
|
-
val mediaType =
|
|
421
|
-
"video" -> "video/mp4".toMediaType()
|
|
422
|
-
else -> "application/gzip".toMediaType()
|
|
423
|
-
}
|
|
496
|
+
private suspend fun uploadToS3(url: String, payload: ByteArray): Boolean {
|
|
497
|
+
val mediaType = "application/gzip".toMediaType()
|
|
424
498
|
|
|
425
499
|
val request = Request.Builder()
|
|
426
500
|
.url(url)
|
|
@@ -435,14 +509,16 @@ class SegmentDispatcher private constructor() {
|
|
|
435
509
|
DiagnosticLog.debugUploadComplete("", response.code, durationMs, 0.0)
|
|
436
510
|
|
|
437
511
|
if (response.code in 200..299) {
|
|
438
|
-
|
|
512
|
+
recordUploadStats(durationMs, true, payload.size.toLong())
|
|
439
513
|
true
|
|
440
514
|
} else {
|
|
515
|
+
recordUploadStats(durationMs, false, payload.size.toLong())
|
|
441
516
|
false
|
|
442
517
|
}
|
|
443
518
|
} catch (e: Exception) {
|
|
444
|
-
DiagnosticLog.
|
|
519
|
+
DiagnosticLog.trace("[SegmentDispatcher] S3 upload exception: ${e.message}")
|
|
445
520
|
DiagnosticLog.fault("[SegmentDispatcher] S3 upload exception: ${e.message}")
|
|
521
|
+
recordUploadStats((System.currentTimeMillis() - startTime).toDouble(), false, payload.size.toLong())
|
|
446
522
|
false
|
|
447
523
|
}
|
|
448
524
|
}
|
|
@@ -454,6 +530,7 @@ class SegmentDispatcher private constructor() {
|
|
|
454
530
|
val body = JSONObject().apply {
|
|
455
531
|
put("actualSizeBytes", upload.payload.size)
|
|
456
532
|
put("timestamp", System.currentTimeMillis())
|
|
533
|
+
put("sdkTelemetry", buildSdkTelemetry(0))
|
|
457
534
|
|
|
458
535
|
if (upload.contentType == "events") {
|
|
459
536
|
put("batchId", batchId)
|
|
@@ -488,21 +565,20 @@ class SegmentDispatcher private constructor() {
|
|
|
488
565
|
rangeStart = 0,
|
|
489
566
|
rangeEnd = 0,
|
|
490
567
|
itemCount = eventCount,
|
|
491
|
-
attempt = 0
|
|
568
|
+
attempt = 0,
|
|
569
|
+
batchNumber = batchNum
|
|
492
570
|
)
|
|
493
571
|
|
|
494
572
|
val presignResponse = requestPresignedUrl(upload)
|
|
495
573
|
if (presignResponse == null) {
|
|
496
|
-
DiagnosticLog.notice("[SegmentDispatcher] ❌ requestPresignedUrl FAILED for ${upload.contentType}")
|
|
497
574
|
DiagnosticLog.caution("[SegmentDispatcher] requestPresignedUrl FAILED for ${upload.contentType}")
|
|
498
575
|
registerFailure()
|
|
499
576
|
scheduleRetryIfNeeded(upload, completion)
|
|
500
577
|
return
|
|
501
578
|
}
|
|
502
579
|
|
|
503
|
-
val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload
|
|
580
|
+
val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload)
|
|
504
581
|
if (!s3ok) {
|
|
505
|
-
DiagnosticLog.notice("[SegmentDispatcher] ❌ uploadToS3 FAILED for ${upload.contentType}")
|
|
506
582
|
DiagnosticLog.caution("[SegmentDispatcher] uploadToS3 FAILED for ${upload.contentType}")
|
|
507
583
|
registerFailure()
|
|
508
584
|
scheduleRetryIfNeeded(upload, completion)
|
|
@@ -513,7 +589,7 @@ class SegmentDispatcher private constructor() {
|
|
|
513
589
|
if (confirmOk) {
|
|
514
590
|
registerSuccess()
|
|
515
591
|
} else {
|
|
516
|
-
DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType}
|
|
592
|
+
DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType}")
|
|
517
593
|
registerFailure()
|
|
518
594
|
}
|
|
519
595
|
completion?.invoke(confirmOk)
|
|
@@ -521,7 +597,7 @@ class SegmentDispatcher private constructor() {
|
|
|
521
597
|
|
|
522
598
|
private fun buildRequest(url: String, body: JSONObject): Request {
|
|
523
599
|
// Log auth state before building request
|
|
524
|
-
DiagnosticLog.
|
|
600
|
+
DiagnosticLog.trace("[SegmentDispatcher] buildRequest: apiToken=${apiToken?.take(15) ?: "NULL"}, credential=${credential?.take(15) ?: "NULL"}, replayId=${currentReplayId?.take(20) ?: "NULL"}")
|
|
525
601
|
|
|
526
602
|
val requestBody = body.toString().toRequestBody("application/json".toMediaType())
|
|
527
603
|
|
|
@@ -541,6 +617,125 @@ class SegmentDispatcher private constructor() {
|
|
|
541
617
|
DiagnosticLog.debugNetworkRequest("POST", url, request.headers.toMultimap().mapValues { it.value.first() })
|
|
542
618
|
return request
|
|
543
619
|
}
|
|
620
|
+
|
|
621
|
+
private fun ingestFinalizeMetrics(metrics: Map<String, Any>?) {
|
|
622
|
+
val crashes = (metrics?.get("crashCount") as? Number)?.toInt() ?: return
|
|
623
|
+
metricsLock.withLock {
|
|
624
|
+
_crashCount = maxOf(_crashCount, crashes)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private fun resetSessionTelemetry() {
|
|
629
|
+
metricsLock.withLock {
|
|
630
|
+
_uploadSuccessCount = 0
|
|
631
|
+
_uploadFailureCount = 0
|
|
632
|
+
_retryAttemptCount = 0
|
|
633
|
+
_circuitBreakerOpenCount = 0
|
|
634
|
+
_memoryEvictionCount = 0
|
|
635
|
+
_offlinePersistCount = 0
|
|
636
|
+
_sessionStartCount = 1
|
|
637
|
+
_crashCount = 0
|
|
638
|
+
_totalBytesUploaded = 0L
|
|
639
|
+
_totalBytesEvicted = 0L
|
|
640
|
+
_totalUploadDurationMs = 0.0
|
|
641
|
+
_uploadDurationSampleCount = 0
|
|
642
|
+
_lastUploadTime = null
|
|
643
|
+
_lastRetryTime = null
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private fun recordUploadStats(durationMs: Double, success: Boolean, bytes: Long) {
|
|
648
|
+
metricsLock.withLock {
|
|
649
|
+
_uploadDurationSampleCount++
|
|
650
|
+
_totalUploadDurationMs += durationMs
|
|
651
|
+
if (success) {
|
|
652
|
+
_totalBytesUploaded += bytes
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private fun buildSdkTelemetry(currentQueueDepth: Int): JSONObject {
|
|
658
|
+
val retryDepth = retryLock.withLock { retryQueue.size }
|
|
659
|
+
|
|
660
|
+
val (
|
|
661
|
+
successCount,
|
|
662
|
+
failureCount,
|
|
663
|
+
retryCount,
|
|
664
|
+
breakerOpenCount,
|
|
665
|
+
memoryEvictions,
|
|
666
|
+
offlinePersists,
|
|
667
|
+
starts,
|
|
668
|
+
crashes,
|
|
669
|
+
avgDurationMs,
|
|
670
|
+
lastUpload,
|
|
671
|
+
lastRetry,
|
|
672
|
+
uploadedBytes,
|
|
673
|
+
evictedBytes,
|
|
674
|
+
) = metricsLock.withLock {
|
|
675
|
+
val avg = if (_uploadDurationSampleCount > 0) {
|
|
676
|
+
_totalUploadDurationMs / _uploadDurationSampleCount.toDouble()
|
|
677
|
+
} else {
|
|
678
|
+
0.0
|
|
679
|
+
}
|
|
680
|
+
TelemetrySnapshot(
|
|
681
|
+
uploadSuccessCount = _uploadSuccessCount,
|
|
682
|
+
uploadFailureCount = _uploadFailureCount,
|
|
683
|
+
retryAttemptCount = _retryAttemptCount,
|
|
684
|
+
circuitBreakerOpenCount = _circuitBreakerOpenCount,
|
|
685
|
+
memoryEvictionCount = _memoryEvictionCount,
|
|
686
|
+
offlinePersistCount = _offlinePersistCount,
|
|
687
|
+
sessionStartCount = _sessionStartCount,
|
|
688
|
+
crashCount = _crashCount,
|
|
689
|
+
avgUploadDurationMs = avg,
|
|
690
|
+
lastUploadTime = _lastUploadTime,
|
|
691
|
+
lastRetryTime = _lastRetryTime,
|
|
692
|
+
totalBytesUploaded = _totalBytesUploaded,
|
|
693
|
+
totalBytesEvicted = _totalBytesEvicted,
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
val totalUploads = successCount + failureCount
|
|
698
|
+
val successRate = if (totalUploads > 0) successCount.toDouble() / totalUploads.toDouble() else 1.0
|
|
699
|
+
|
|
700
|
+
return JSONObject().apply {
|
|
701
|
+
put("uploadSuccessCount", successCount)
|
|
702
|
+
put("uploadFailureCount", failureCount)
|
|
703
|
+
put("retryAttemptCount", retryCount)
|
|
704
|
+
put("circuitBreakerOpenCount", breakerOpenCount)
|
|
705
|
+
put("memoryEvictionCount", memoryEvictions)
|
|
706
|
+
put("offlinePersistCount", offlinePersists)
|
|
707
|
+
put("sessionStartCount", starts)
|
|
708
|
+
put("crashCount", crashes)
|
|
709
|
+
put("uploadSuccessRate", successRate)
|
|
710
|
+
put("avgUploadDurationMs", avgDurationMs)
|
|
711
|
+
put("currentQueueDepth", currentQueueDepth + retryDepth)
|
|
712
|
+
put("lastUploadTime", lastUpload ?: JSONObject.NULL)
|
|
713
|
+
put("lastRetryTime", lastRetry ?: JSONObject.NULL)
|
|
714
|
+
put("totalBytesUploaded", uploadedBytes)
|
|
715
|
+
put("totalBytesEvicted", evictedBytes)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
fun sdkTelemetrySnapshot(currentQueueDepth: Int = 0): Map<String, Any?> {
|
|
720
|
+
val payload = buildSdkTelemetry(currentQueueDepth)
|
|
721
|
+
return mapOf(
|
|
722
|
+
"uploadSuccessCount" to payload.optInt("uploadSuccessCount", 0),
|
|
723
|
+
"uploadFailureCount" to payload.optInt("uploadFailureCount", 0),
|
|
724
|
+
"retryAttemptCount" to payload.optInt("retryAttemptCount", 0),
|
|
725
|
+
"circuitBreakerOpenCount" to payload.optInt("circuitBreakerOpenCount", 0),
|
|
726
|
+
"memoryEvictionCount" to payload.optInt("memoryEvictionCount", 0),
|
|
727
|
+
"offlinePersistCount" to payload.optInt("offlinePersistCount", 0),
|
|
728
|
+
"sessionStartCount" to payload.optInt("sessionStartCount", 0),
|
|
729
|
+
"crashCount" to payload.optInt("crashCount", 0),
|
|
730
|
+
"uploadSuccessRate" to payload.optDouble("uploadSuccessRate", 1.0),
|
|
731
|
+
"avgUploadDurationMs" to payload.optDouble("avgUploadDurationMs", 0.0),
|
|
732
|
+
"currentQueueDepth" to payload.optInt("currentQueueDepth", 0),
|
|
733
|
+
"lastUploadTime" to (payload.opt("lastUploadTime").takeUnless { it == JSONObject.NULL } as? Number)?.toLong(),
|
|
734
|
+
"lastRetryTime" to (payload.opt("lastRetryTime").takeUnless { it == JSONObject.NULL } as? Number)?.toLong(),
|
|
735
|
+
"totalBytesUploaded" to payload.optLong("totalBytesUploaded", 0),
|
|
736
|
+
"totalBytesEvicted" to payload.optLong("totalBytesEvicted", 0),
|
|
737
|
+
)
|
|
738
|
+
}
|
|
544
739
|
}
|
|
545
740
|
|
|
546
741
|
private data class PendingUpload(
|
|
@@ -550,10 +745,27 @@ private data class PendingUpload(
|
|
|
550
745
|
val rangeStart: Long,
|
|
551
746
|
val rangeEnd: Long,
|
|
552
747
|
val itemCount: Int,
|
|
553
|
-
val attempt: Int
|
|
748
|
+
val attempt: Int,
|
|
749
|
+
val batchNumber: Int = 0
|
|
554
750
|
)
|
|
555
751
|
|
|
556
752
|
private data class PresignResponse(
|
|
557
753
|
val presignedUrl: String,
|
|
558
754
|
val batchId: String
|
|
559
755
|
)
|
|
756
|
+
|
|
757
|
+
private data class TelemetrySnapshot(
|
|
758
|
+
val uploadSuccessCount: Int,
|
|
759
|
+
val uploadFailureCount: Int,
|
|
760
|
+
val retryAttemptCount: Int,
|
|
761
|
+
val circuitBreakerOpenCount: Int,
|
|
762
|
+
val memoryEvictionCount: Int,
|
|
763
|
+
val offlinePersistCount: Int,
|
|
764
|
+
val sessionStartCount: Int,
|
|
765
|
+
val crashCount: Int,
|
|
766
|
+
val avgUploadDurationMs: Double,
|
|
767
|
+
val lastUploadTime: Long?,
|
|
768
|
+
val lastRetryTime: Long?,
|
|
769
|
+
val totalBytesUploaded: Long,
|
|
770
|
+
val totalBytesEvicted: Long,
|
|
771
|
+
)
|