@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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
  3. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
  4. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  5. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  6. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
  7. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
  8. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
  9. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
  10. package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
  11. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  12. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
  13. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  14. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
  15. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  16. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  17. package/ios/Engine/DeviceRegistrar.swift +13 -3
  18. package/ios/Engine/RejourneyImpl.swift +202 -133
  19. package/ios/Recording/AnrSentinel.swift +58 -25
  20. package/ios/Recording/InteractionRecorder.swift +29 -0
  21. package/ios/Recording/RejourneyURLProtocol.swift +168 -0
  22. package/ios/Recording/ReplayOrchestrator.swift +241 -147
  23. package/ios/Recording/SegmentDispatcher.swift +155 -13
  24. package/ios/Recording/SpecialCases.swift +614 -0
  25. package/ios/Recording/StabilityMonitor.swift +42 -34
  26. package/ios/Recording/TelemetryPipeline.swift +38 -3
  27. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  28. package/ios/Recording/VisualCapture.swift +104 -28
  29. package/ios/Rejourney.mm +27 -8
  30. package/ios/Utility/ImageBlur.swift +0 -1
  31. package/lib/commonjs/index.js +32 -20
  32. package/lib/commonjs/sdk/autoTracking.js +162 -11
  33. package/lib/commonjs/sdk/constants.js +2 -2
  34. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  35. package/lib/commonjs/sdk/utils.js +1 -1
  36. package/lib/module/index.js +32 -20
  37. package/lib/module/sdk/autoTracking.js +162 -11
  38. package/lib/module/sdk/constants.js +2 -2
  39. package/lib/module/sdk/networkInterceptor.js +84 -4
  40. package/lib/module/sdk/utils.js +1 -1
  41. package/lib/typescript/NativeRejourney.d.ts +5 -2
  42. package/lib/typescript/sdk/autoTracking.d.ts +3 -1
  43. package/lib/typescript/sdk/constants.d.ts +2 -2
  44. package/lib/typescript/types/index.d.ts +15 -8
  45. package/package.json +4 -4
  46. package/src/NativeRejourney.ts +8 -5
  47. package/src/index.ts +46 -29
  48. package/src/sdk/autoTracking.ts +176 -11
  49. package/src/sdk/constants.ts +2 -2
  50. package/src/sdk/networkInterceptor.ts +110 -1
  51. package/src/sdk/utils.ts +1 -1
  52. 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
- // Metrics
62
- var uploadSuccessCount = 0
63
- var uploadFailureCount = 0
64
- var totalBytesUploaded = 0L
65
- var circuitBreakerOpenCount = 0
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.notice("[SegmentDispatcher] transmitFrameBundle: sid=${sid?.take(12) ?: "null"}, canUpload=$canUpload, frames=$frameCount, bytes=${payload.size}")
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.caution("[SegmentDispatcher] transmitFrameBundle: rejected - sid=${sid != null}, canUpload=$canUpload")
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
- uploadFailureCount++
340
+ metricsLock.withLock {
341
+ _uploadFailureCount++
342
+ }
275
343
  if (consecutiveFailures >= circuitBreakerThreshold) {
276
- if (!circuitOpen) circuitBreakerOpenCount++
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
- uploadSuccessCount++
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.notice("[SegmentDispatcher] scheduleUpload: active=$active, type=${upload.contentType}, items=${upload.itemCount}")
364
+ DiagnosticLog.trace("[SegmentDispatcher] scheduleUpload: active=$active, type=${upload.contentType}, items=${upload.itemCount}")
289
365
  if (!active) {
290
- DiagnosticLog.caution("[SegmentDispatcher] scheduleUpload: rejected - not active")
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, upload.contentType)
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", batchSeqNumber)
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.notice("[SegmentDispatcher] presign: 402 Payment Required - billing blocked")
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.notice("[SegmentDispatcher] presign failed: status=${response.code} body=$bodyPreview")
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.notice("[SegmentDispatcher] presign exception (${durationMs.toLong()}ms): ${e.javaClass.simpleName}: ${e.message}")
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, contentType: String): Boolean {
420
- val mediaType = when (contentType) {
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
- totalBytesUploaded += payload.size
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.notice("[SegmentDispatcher] S3 upload exception: ${e.message}")
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, upload.contentType)
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} (batchId=${presignResponse.batchId})")
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.notice("[SegmentDispatcher] buildRequest: apiToken=${apiToken?.take(15) ?: "NULL"}, credential=${credential?.take(15) ?: "NULL"}, replayId=${currentReplayId?.take(20) ?: "NULL"}")
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
+ )