@rejourneyco/react-native 1.0.7 → 1.0.8

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 (29) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +20 -18
  3. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +28 -0
  4. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +42 -33
  5. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +242 -34
  6. package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
  7. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +6 -4
  8. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +156 -64
  9. package/ios/Engine/RejourneyImpl.swift +3 -18
  10. package/ios/Recording/InteractionRecorder.swift +28 -0
  11. package/ios/Recording/ReplayOrchestrator.swift +50 -17
  12. package/ios/Recording/SegmentDispatcher.swift +147 -13
  13. package/ios/Recording/SpecialCases.swift +614 -0
  14. package/ios/Recording/StabilityMonitor.swift +2 -2
  15. package/ios/Recording/TelemetryPipeline.swift +21 -3
  16. package/ios/Recording/VisualCapture.swift +50 -20
  17. package/lib/commonjs/index.js +4 -5
  18. package/lib/commonjs/sdk/constants.js +2 -2
  19. package/lib/commonjs/sdk/utils.js +1 -1
  20. package/lib/module/index.js +4 -5
  21. package/lib/module/sdk/constants.js +2 -2
  22. package/lib/module/sdk/utils.js +1 -1
  23. package/lib/typescript/sdk/constants.d.ts +2 -2
  24. package/lib/typescript/types/index.d.ts +1 -6
  25. package/package.json +2 -2
  26. package/src/index.ts +9 -10
  27. package/src/sdk/constants.ts +2 -2
  28. package/src/sdk/utils.ts +1 -1
  29. package/src/types/index.ts +1 -6
@@ -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,18 @@ class SegmentDispatcher private constructor() {
201
258
  concludedAt: Long,
202
259
  backgroundDurationMs: Long,
203
260
  metrics: Map<String, Any>?,
261
+ currentQueueDepth: Int = 0,
204
262
  completion: (Boolean) -> Unit
205
263
  ) {
206
264
  val url = "$endpoint/api/ingest/session/end"
265
+ ingestFinalizeMetrics(metrics)
207
266
 
208
267
  val body = JSONObject().apply {
209
268
  put("sessionId", replayId)
210
269
  put("endedAt", concludedAt)
211
270
  if (backgroundDurationMs > 0) put("totalBackgroundTimeMs", backgroundDurationMs)
212
271
  metrics?.let { put("metrics", JSONObject(it)) }
272
+ put("sdkTelemetry", buildSdkTelemetry(currentQueueDepth))
213
273
  }
214
274
 
215
275
  val request = buildRequest(url, body)
@@ -257,6 +317,7 @@ class SegmentDispatcher private constructor() {
257
317
  }
258
318
  }
259
319
 
320
+ @Synchronized
260
321
  private fun canUploadNow(): Boolean {
261
322
  if (billingBlocked) return false
262
323
  if (circuitOpen) {
@@ -269,25 +330,36 @@ class SegmentDispatcher private constructor() {
269
330
  return true
270
331
  }
271
332
 
333
+ @Synchronized
272
334
  private fun registerFailure() {
273
335
  consecutiveFailures++
274
- uploadFailureCount++
336
+ metricsLock.withLock {
337
+ _uploadFailureCount++
338
+ }
275
339
  if (consecutiveFailures >= circuitBreakerThreshold) {
276
- if (!circuitOpen) circuitBreakerOpenCount++
340
+ if (!circuitOpen) {
341
+ metricsLock.withLock {
342
+ _circuitBreakerOpenCount++
343
+ }
344
+ }
277
345
  circuitOpen = true
278
346
  circuitOpenTime = System.currentTimeMillis()
279
347
  }
280
348
  }
281
349
 
350
+ @Synchronized
282
351
  private fun registerSuccess() {
283
352
  consecutiveFailures = 0
284
- uploadSuccessCount++
353
+ metricsLock.withLock {
354
+ _uploadSuccessCount++
355
+ _lastUploadTime = System.currentTimeMillis()
356
+ }
285
357
  }
286
358
 
287
359
  private fun scheduleUpload(upload: PendingUpload, completion: ((Boolean) -> Unit)?) {
288
- DiagnosticLog.notice("[SegmentDispatcher] scheduleUpload: active=$active, type=${upload.contentType}, items=${upload.itemCount}")
360
+ DiagnosticLog.trace("[SegmentDispatcher] scheduleUpload: active=$active, type=${upload.contentType}, items=${upload.itemCount}")
289
361
  if (!active) {
290
- DiagnosticLog.caution("[SegmentDispatcher] scheduleUpload: rejected - not active")
362
+ DiagnosticLog.trace("[SegmentDispatcher] scheduleUpload: rejected - not active")
291
363
  completion?.invoke(false)
292
364
  return
293
365
  }
@@ -304,16 +376,14 @@ class SegmentDispatcher private constructor() {
304
376
 
305
377
  val presignResponse = requestPresignedUrl(upload)
306
378
  if (presignResponse == null) {
307
- DiagnosticLog.notice("[SegmentDispatcher] ❌ requestPresignedUrl FAILED for ${upload.contentType}")
308
379
  DiagnosticLog.caution("[SegmentDispatcher] requestPresignedUrl FAILED for ${upload.contentType}")
309
380
  registerFailure()
310
381
  scheduleRetryIfNeeded(upload, completion)
311
382
  return
312
383
  }
313
384
 
314
- val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload, upload.contentType)
385
+ val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload)
315
386
  if (!s3ok) {
316
- DiagnosticLog.notice("[SegmentDispatcher] ❌ uploadToS3 FAILED for ${upload.contentType}")
317
387
  DiagnosticLog.caution("[SegmentDispatcher] uploadToS3 FAILED for ${upload.contentType}")
318
388
  registerFailure()
319
389
  scheduleRetryIfNeeded(upload, completion)
@@ -324,7 +394,6 @@ class SegmentDispatcher private constructor() {
324
394
  if (confirmOk) {
325
395
  registerSuccess()
326
396
  } else {
327
- DiagnosticLog.notice("[SegmentDispatcher] ❌ confirmBatchComplete FAILED for ${upload.contentType}")
328
397
  DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType}")
329
398
  registerFailure()
330
399
  }
@@ -337,6 +406,10 @@ class SegmentDispatcher private constructor() {
337
406
  retryLock.withLock {
338
407
  retryQueue.add(retry)
339
408
  }
409
+ metricsLock.withLock {
410
+ _retryAttemptCount++
411
+ _lastRetryTime = System.currentTimeMillis()
412
+ }
340
413
  }
341
414
  completion?.invoke(false)
342
415
  }
@@ -362,7 +435,7 @@ class SegmentDispatcher private constructor() {
362
435
 
363
436
  if (upload.contentType == "events") {
364
437
  put("contentType", "events")
365
- put("batchNumber", batchSeqNumber)
438
+ put("batchNumber", upload.batchNumber)
366
439
  put("isSampledIn", isSampledIn) // Server-side enforcement
367
440
  } else {
368
441
  put("kind", upload.contentType)
@@ -384,14 +457,14 @@ class SegmentDispatcher private constructor() {
384
457
  DiagnosticLog.debugPresignResponse(response.code, null, null, durationMs)
385
458
 
386
459
  if (response.code == 402) {
387
- DiagnosticLog.notice("[SegmentDispatcher] presign: 402 Payment Required - billing blocked")
460
+ DiagnosticLog.caution("[SegmentDispatcher] presign: 402 Payment Required - billing blocked")
388
461
  billingBlocked = true
389
462
  return null
390
463
  }
391
464
 
392
465
  if (response.code != 200 || responseBody == null) {
393
466
  val bodyPreview = responseBody?.take(300) ?: "null"
394
- DiagnosticLog.notice("[SegmentDispatcher] presign failed: status=${response.code} body=$bodyPreview")
467
+ DiagnosticLog.caution("[SegmentDispatcher] presign failed: status=${response.code} body=$bodyPreview")
395
468
  return null
396
469
  }
397
470
 
@@ -410,17 +483,14 @@ class SegmentDispatcher private constructor() {
410
483
  PresignResponse(presignedUrl, batchId)
411
484
  } catch (e: Exception) {
412
485
  val durationMs = (System.currentTimeMillis() - startTime).toDouble()
413
- DiagnosticLog.notice("[SegmentDispatcher] presign exception (${durationMs.toLong()}ms): ${e.javaClass.simpleName}: ${e.message}")
486
+ DiagnosticLog.trace("[SegmentDispatcher] presign exception (${durationMs.toLong()}ms): ${e.javaClass.simpleName}: ${e.message}")
414
487
  DiagnosticLog.fault("[SegmentDispatcher] presign exception: ${e.message}")
415
488
  null
416
489
  }
417
490
  }
418
491
 
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
- }
492
+ private suspend fun uploadToS3(url: String, payload: ByteArray): Boolean {
493
+ val mediaType = "application/gzip".toMediaType()
424
494
 
425
495
  val request = Request.Builder()
426
496
  .url(url)
@@ -435,14 +505,16 @@ class SegmentDispatcher private constructor() {
435
505
  DiagnosticLog.debugUploadComplete("", response.code, durationMs, 0.0)
436
506
 
437
507
  if (response.code in 200..299) {
438
- totalBytesUploaded += payload.size
508
+ recordUploadStats(durationMs, true, payload.size.toLong())
439
509
  true
440
510
  } else {
511
+ recordUploadStats(durationMs, false, payload.size.toLong())
441
512
  false
442
513
  }
443
514
  } catch (e: Exception) {
444
- DiagnosticLog.notice("[SegmentDispatcher] S3 upload exception: ${e.message}")
515
+ DiagnosticLog.trace("[SegmentDispatcher] S3 upload exception: ${e.message}")
445
516
  DiagnosticLog.fault("[SegmentDispatcher] S3 upload exception: ${e.message}")
517
+ recordUploadStats((System.currentTimeMillis() - startTime).toDouble(), false, payload.size.toLong())
446
518
  false
447
519
  }
448
520
  }
@@ -454,6 +526,7 @@ class SegmentDispatcher private constructor() {
454
526
  val body = JSONObject().apply {
455
527
  put("actualSizeBytes", upload.payload.size)
456
528
  put("timestamp", System.currentTimeMillis())
529
+ put("sdkTelemetry", buildSdkTelemetry(0))
457
530
 
458
531
  if (upload.contentType == "events") {
459
532
  put("batchId", batchId)
@@ -488,21 +561,20 @@ class SegmentDispatcher private constructor() {
488
561
  rangeStart = 0,
489
562
  rangeEnd = 0,
490
563
  itemCount = eventCount,
491
- attempt = 0
564
+ attempt = 0,
565
+ batchNumber = batchNum
492
566
  )
493
567
 
494
568
  val presignResponse = requestPresignedUrl(upload)
495
569
  if (presignResponse == null) {
496
- DiagnosticLog.notice("[SegmentDispatcher] ❌ requestPresignedUrl FAILED for ${upload.contentType}")
497
570
  DiagnosticLog.caution("[SegmentDispatcher] requestPresignedUrl FAILED for ${upload.contentType}")
498
571
  registerFailure()
499
572
  scheduleRetryIfNeeded(upload, completion)
500
573
  return
501
574
  }
502
575
 
503
- val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload, upload.contentType)
576
+ val s3ok = uploadToS3(presignResponse.presignedUrl, upload.payload)
504
577
  if (!s3ok) {
505
- DiagnosticLog.notice("[SegmentDispatcher] ❌ uploadToS3 FAILED for ${upload.contentType}")
506
578
  DiagnosticLog.caution("[SegmentDispatcher] uploadToS3 FAILED for ${upload.contentType}")
507
579
  registerFailure()
508
580
  scheduleRetryIfNeeded(upload, completion)
@@ -513,7 +585,7 @@ class SegmentDispatcher private constructor() {
513
585
  if (confirmOk) {
514
586
  registerSuccess()
515
587
  } else {
516
- DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType} (batchId=${presignResponse.batchId})")
588
+ DiagnosticLog.caution("[SegmentDispatcher] confirmBatchComplete FAILED for ${upload.contentType}")
517
589
  registerFailure()
518
590
  }
519
591
  completion?.invoke(confirmOk)
@@ -521,7 +593,7 @@ class SegmentDispatcher private constructor() {
521
593
 
522
594
  private fun buildRequest(url: String, body: JSONObject): Request {
523
595
  // 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"}")
596
+ DiagnosticLog.trace("[SegmentDispatcher] buildRequest: apiToken=${apiToken?.take(15) ?: "NULL"}, credential=${credential?.take(15) ?: "NULL"}, replayId=${currentReplayId?.take(20) ?: "NULL"}")
525
597
 
526
598
  val requestBody = body.toString().toRequestBody("application/json".toMediaType())
527
599
 
@@ -541,6 +613,125 @@ class SegmentDispatcher private constructor() {
541
613
  DiagnosticLog.debugNetworkRequest("POST", url, request.headers.toMultimap().mapValues { it.value.first() })
542
614
  return request
543
615
  }
616
+
617
+ private fun ingestFinalizeMetrics(metrics: Map<String, Any>?) {
618
+ val crashes = (metrics?.get("crashCount") as? Number)?.toInt() ?: return
619
+ metricsLock.withLock {
620
+ _crashCount = maxOf(_crashCount, crashes)
621
+ }
622
+ }
623
+
624
+ private fun resetSessionTelemetry() {
625
+ metricsLock.withLock {
626
+ _uploadSuccessCount = 0
627
+ _uploadFailureCount = 0
628
+ _retryAttemptCount = 0
629
+ _circuitBreakerOpenCount = 0
630
+ _memoryEvictionCount = 0
631
+ _offlinePersistCount = 0
632
+ _sessionStartCount = 1
633
+ _crashCount = 0
634
+ _totalBytesUploaded = 0L
635
+ _totalBytesEvicted = 0L
636
+ _totalUploadDurationMs = 0.0
637
+ _uploadDurationSampleCount = 0
638
+ _lastUploadTime = null
639
+ _lastRetryTime = null
640
+ }
641
+ }
642
+
643
+ private fun recordUploadStats(durationMs: Double, success: Boolean, bytes: Long) {
644
+ metricsLock.withLock {
645
+ _uploadDurationSampleCount++
646
+ _totalUploadDurationMs += durationMs
647
+ if (success) {
648
+ _totalBytesUploaded += bytes
649
+ }
650
+ }
651
+ }
652
+
653
+ private fun buildSdkTelemetry(currentQueueDepth: Int): JSONObject {
654
+ val retryDepth = retryLock.withLock { retryQueue.size }
655
+
656
+ val (
657
+ successCount,
658
+ failureCount,
659
+ retryCount,
660
+ breakerOpenCount,
661
+ memoryEvictions,
662
+ offlinePersists,
663
+ starts,
664
+ crashes,
665
+ avgDurationMs,
666
+ lastUpload,
667
+ lastRetry,
668
+ uploadedBytes,
669
+ evictedBytes,
670
+ ) = metricsLock.withLock {
671
+ val avg = if (_uploadDurationSampleCount > 0) {
672
+ _totalUploadDurationMs / _uploadDurationSampleCount.toDouble()
673
+ } else {
674
+ 0.0
675
+ }
676
+ TelemetrySnapshot(
677
+ uploadSuccessCount = _uploadSuccessCount,
678
+ uploadFailureCount = _uploadFailureCount,
679
+ retryAttemptCount = _retryAttemptCount,
680
+ circuitBreakerOpenCount = _circuitBreakerOpenCount,
681
+ memoryEvictionCount = _memoryEvictionCount,
682
+ offlinePersistCount = _offlinePersistCount,
683
+ sessionStartCount = _sessionStartCount,
684
+ crashCount = _crashCount,
685
+ avgUploadDurationMs = avg,
686
+ lastUploadTime = _lastUploadTime,
687
+ lastRetryTime = _lastRetryTime,
688
+ totalBytesUploaded = _totalBytesUploaded,
689
+ totalBytesEvicted = _totalBytesEvicted,
690
+ )
691
+ }
692
+
693
+ val totalUploads = successCount + failureCount
694
+ val successRate = if (totalUploads > 0) successCount.toDouble() / totalUploads.toDouble() else 1.0
695
+
696
+ return JSONObject().apply {
697
+ put("uploadSuccessCount", successCount)
698
+ put("uploadFailureCount", failureCount)
699
+ put("retryAttemptCount", retryCount)
700
+ put("circuitBreakerOpenCount", breakerOpenCount)
701
+ put("memoryEvictionCount", memoryEvictions)
702
+ put("offlinePersistCount", offlinePersists)
703
+ put("sessionStartCount", starts)
704
+ put("crashCount", crashes)
705
+ put("uploadSuccessRate", successRate)
706
+ put("avgUploadDurationMs", avgDurationMs)
707
+ put("currentQueueDepth", currentQueueDepth + retryDepth)
708
+ put("lastUploadTime", lastUpload ?: JSONObject.NULL)
709
+ put("lastRetryTime", lastRetry ?: JSONObject.NULL)
710
+ put("totalBytesUploaded", uploadedBytes)
711
+ put("totalBytesEvicted", evictedBytes)
712
+ }
713
+ }
714
+
715
+ fun sdkTelemetrySnapshot(currentQueueDepth: Int = 0): Map<String, Any?> {
716
+ val payload = buildSdkTelemetry(currentQueueDepth)
717
+ return mapOf(
718
+ "uploadSuccessCount" to payload.optInt("uploadSuccessCount", 0),
719
+ "uploadFailureCount" to payload.optInt("uploadFailureCount", 0),
720
+ "retryAttemptCount" to payload.optInt("retryAttemptCount", 0),
721
+ "circuitBreakerOpenCount" to payload.optInt("circuitBreakerOpenCount", 0),
722
+ "memoryEvictionCount" to payload.optInt("memoryEvictionCount", 0),
723
+ "offlinePersistCount" to payload.optInt("offlinePersistCount", 0),
724
+ "sessionStartCount" to payload.optInt("sessionStartCount", 0),
725
+ "crashCount" to payload.optInt("crashCount", 0),
726
+ "uploadSuccessRate" to payload.optDouble("uploadSuccessRate", 1.0),
727
+ "avgUploadDurationMs" to payload.optDouble("avgUploadDurationMs", 0.0),
728
+ "currentQueueDepth" to payload.optInt("currentQueueDepth", 0),
729
+ "lastUploadTime" to (payload.opt("lastUploadTime").takeUnless { it == JSONObject.NULL } as? Number)?.toLong(),
730
+ "lastRetryTime" to (payload.opt("lastRetryTime").takeUnless { it == JSONObject.NULL } as? Number)?.toLong(),
731
+ "totalBytesUploaded" to payload.optLong("totalBytesUploaded", 0),
732
+ "totalBytesEvicted" to payload.optLong("totalBytesEvicted", 0),
733
+ )
734
+ }
544
735
  }
545
736
 
546
737
  private data class PendingUpload(
@@ -550,10 +741,27 @@ private data class PendingUpload(
550
741
  val rangeStart: Long,
551
742
  val rangeEnd: Long,
552
743
  val itemCount: Int,
553
- val attempt: Int
744
+ val attempt: Int,
745
+ val batchNumber: Int = 0
554
746
  )
555
747
 
556
748
  private data class PresignResponse(
557
749
  val presignedUrl: String,
558
750
  val batchId: String
559
751
  )
752
+
753
+ private data class TelemetrySnapshot(
754
+ val uploadSuccessCount: Int,
755
+ val uploadFailureCount: Int,
756
+ val retryAttemptCount: Int,
757
+ val circuitBreakerOpenCount: Int,
758
+ val memoryEvictionCount: Int,
759
+ val offlinePersistCount: Int,
760
+ val sessionStartCount: Int,
761
+ val crashCount: Int,
762
+ val avgUploadDurationMs: Double,
763
+ val lastUploadTime: Long?,
764
+ val lastRetryTime: Long?,
765
+ val totalBytesUploaded: Long,
766
+ val totalBytesEvicted: Long,
767
+ )