@rejourneyco/react-native 1.0.10 → 1.0.12

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 CHANGED
@@ -64,22 +64,29 @@ Rejourney.trackScreen('Custom Screen Name');
64
64
 
65
65
  ## Custom Events & Metadata
66
66
 
67
- You can track custom events and assign metadata to sessions to filter and segment them later.
67
+ Track user actions and attach session-level context for filtering and segmentation in the dashboard.
68
68
 
69
69
  ```typescript
70
70
  import { Rejourney } from '@rejourneyco/react-native';
71
71
 
72
- // Log custom events
73
- Rejourney.logEvent('button_clicked', { buttonName: 'signup' });
72
+ // Log custom events with optional properties
73
+ Rejourney.logEvent('signup_completed');
74
+ Rejourney.logEvent('purchase_completed', {
75
+ plan: 'pro',
76
+ amount: 29.99
77
+ });
74
78
 
75
- // Add custom session metadata
79
+ // Attach session-level metadata (key-value context)
76
80
  Rejourney.setMetadata('plan', 'premium');
77
81
  Rejourney.setMetadata({
78
- role: 'tester',
79
- ab_test_group: 'A'
82
+ role: 'admin',
83
+ ab_variant: 'checkout_v2'
80
84
  });
81
85
  ```
82
86
 
87
+ **Events** = things that happened (actions, timestamped, can occur multiple times)
88
+ **Metadata** = who the user is / what state they're in (session-level, one value per key)
89
+
83
90
  ## API Reference & Compatibility
84
91
 
85
92
  Rejourney supports both a standardized `Rejourney.` namespace and standalone function exports (AKA calls). Both are fully supported.
@@ -483,6 +483,10 @@ class ReplayOrchestrator private constructor(private val context: Context) {
483
483
 
484
484
  fun logScreenView(screenId: String) {
485
485
  if (screenId.isEmpty()) return
486
+ if (visitedScreens.size >= 500) {
487
+ val excess = visitedScreens.size - 250
488
+ repeat(excess) { visitedScreens.removeAt(0) }
489
+ }
486
490
  visitedScreens.add(screenId)
487
491
  currentScreenName = screenId
488
492
  if (hierarchyCaptureEnabled) captureHierarchy()
@@ -125,15 +125,17 @@ class SegmentDispatcher private constructor() {
125
125
  private val scope = CoroutineScope(workerExecutor.asCoroutineDispatcher() + SupervisorJob())
126
126
 
127
127
  private val httpClient: OkHttpClient = OkHttpClient.Builder()
128
- .connectTimeout(5, TimeUnit.SECONDS) // Short timeout for debugging
128
+ .connectTimeout(5, TimeUnit.SECONDS)
129
129
  .readTimeout(10, TimeUnit.SECONDS)
130
130
  .writeTimeout(10, TimeUnit.SECONDS)
131
- // Mirror iOS URLProtocol: ensure native upload/auth traffic is captured
132
- .addInterceptor(RejourneyNetworkInterceptor())
131
+ // Intentionally NO RejourneyNetworkInterceptor here: intercepting our
132
+ // own upload traffic creates redundant network events, wastes bandwidth,
133
+ // and can cause circular upload→intercept→upload chains.
133
134
  .build()
134
135
 
135
136
  private val retryQueue = mutableListOf<PendingUpload>()
136
137
  private val retryLock = ReentrantLock()
138
+ private val maxRetryQueueSize = 20
137
139
  private var active = true
138
140
 
139
141
  fun configure(replayId: String, apiToken: String?, credential: String?, projectId: String?, isSampledIn: Boolean = true) {
@@ -411,6 +413,9 @@ class SegmentDispatcher private constructor() {
411
413
  if (upload.attempt < 3) {
412
414
  val retry = upload.copy(attempt = upload.attempt + 1)
413
415
  retryLock.withLock {
416
+ if (retryQueue.size >= maxRetryQueueSize) {
417
+ retryQueue.removeAt(0)
418
+ }
414
419
  retryQueue.add(retry)
415
420
  }
416
421
  metricsLock.withLock {
@@ -18,22 +18,26 @@ import Foundation
18
18
 
19
19
  /// Intercepts URLSession network traffic globally for Rejourney Session Replay.
20
20
  @objc(RejourneyURLProtocol)
21
- public class RejourneyURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
21
+ public class RejourneyURLProtocol: URLProtocol {
22
22
 
23
- // We tag requests that we've already handled so we don't intercept them repeatedly.
24
23
  private static let _handledKey = "co.rejourney.handled"
25
24
 
26
25
  private var _dataTask: URLSessionDataTask?
27
26
  private var _startMs: Int64 = 0
28
27
  private var _endMs: Int64 = 0
29
- private var _responseData: Data?
30
28
  private var _response: URLResponse?
31
29
  private var _error: Error?
32
30
 
33
- // Session used to forward the intercepted request execution
34
- private lazy var _session: URLSession = {
35
- let config = URLSessionConfiguration.default
36
- return URLSession(configuration: config, delegate: self, delegateQueue: nil)
31
+ /// Shared forwarding session. Uses ephemeral config with protocol classes
32
+ /// stripped to prevent self-interception, and a delegate adapter that routes
33
+ /// callbacks to the correct RejourneyURLProtocol instance via a task map.
34
+ /// This avoids the per-instance URLSession retain cycle that previously
35
+ /// leaked every intercepted request (~1-3MB each).
36
+ private static let _delegateAdapter = SessionDelegateAdapter()
37
+ private static let _sharedSession: URLSession = {
38
+ let cfg = URLSessionConfiguration.ephemeral
39
+ cfg.protocolClasses = []
40
+ return URLSession(configuration: cfg, delegate: _delegateAdapter, delegateQueue: nil)
37
41
  }()
38
42
 
39
43
  @objc public static func enable() {
@@ -130,30 +134,32 @@ public class RejourneyURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessi
130
134
  URLProtocol.setProperty(true, forKey: RejourneyURLProtocol._handledKey, in: request)
131
135
 
132
136
  _startMs = Int64(Date().timeIntervalSince1970 * 1000)
133
- _dataTask = _session.dataTask(with: request as URLRequest)
134
- _dataTask?.resume()
137
+ let task = Self._sharedSession.dataTask(with: request as URLRequest)
138
+ Self._delegateAdapter.register(task: task, protocol: self)
139
+ _dataTask = task
140
+ task.resume()
135
141
  }
136
142
 
137
143
  public override func stopLoading() {
138
- _dataTask?.cancel()
144
+ if let task = _dataTask {
145
+ Self._delegateAdapter.unregister(task: task)
146
+ task.cancel()
147
+ }
139
148
  _dataTask = nil
140
149
  }
141
150
 
142
- // MARK: - URLSessionDataDelegate
151
+ // MARK: - Callbacks from SessionDelegateAdapter
143
152
 
144
- public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
153
+ fileprivate func handleDidReceiveData(_ data: Data) {
145
154
  client?.urlProtocol(self, didLoad: data)
146
155
  }
147
156
 
148
- public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
157
+ fileprivate func handleDidReceiveResponse(_ response: URLResponse) {
149
158
  _response = response
150
159
  client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
151
- completionHandler(.allow)
152
160
  }
153
161
 
154
- // MARK: - URLSessionTaskDelegate
155
-
156
- public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
162
+ fileprivate func handleDidComplete(error: Error?) {
157
163
  _endMs = Int64(Date().timeIntervalSince1970 * 1000)
158
164
  _error = error
159
165
 
@@ -162,7 +168,9 @@ public class RejourneyURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessi
162
168
  } else {
163
169
  client?.urlProtocolDidFinishLoading(self)
164
170
  }
165
-
171
+ }
172
+
173
+ fileprivate func handleDidCompleteLogging(task: URLSessionTask) {
166
174
  _logRequest(task: task)
167
175
  }
168
176
 
@@ -214,3 +222,45 @@ public class RejourneyURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessi
214
222
  TelemetryPipeline.shared.recordNetworkEvent(details: event)
215
223
  }
216
224
  }
225
+
226
+ /// Routes URLSession delegate callbacks to the correct RejourneyURLProtocol
227
+ /// instance using a task-to-protocol map. Uses weak references to avoid
228
+ /// retaining protocol instances that have been stopped by the URL loading system.
229
+ private final class SessionDelegateAdapter: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate {
230
+ private let _lock = NSLock()
231
+ private let _taskMap = NSMapTable<URLSessionTask, RejourneyURLProtocol>.strongToWeakObjects()
232
+
233
+ func register(task: URLSessionTask, protocol proto: RejourneyURLProtocol) {
234
+ _lock.lock()
235
+ _taskMap.setObject(proto, forKey: task)
236
+ _lock.unlock()
237
+ }
238
+
239
+ func unregister(task: URLSessionTask) {
240
+ _lock.lock()
241
+ _taskMap.removeObject(forKey: task)
242
+ _lock.unlock()
243
+ }
244
+
245
+ private func proto(for task: URLSessionTask) -> RejourneyURLProtocol? {
246
+ _lock.lock()
247
+ defer { _lock.unlock() }
248
+ return _taskMap.object(forKey: task)
249
+ }
250
+
251
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
252
+ proto(for: dataTask)?.handleDidReceiveData(data)
253
+ }
254
+
255
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
256
+ proto(for: dataTask)?.handleDidReceiveResponse(response)
257
+ completionHandler(.allow)
258
+ }
259
+
260
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
261
+ guard let p = proto(for: task) else { return }
262
+ p.handleDidComplete(error: error)
263
+ p.handleDidCompleteLogging(task: task)
264
+ unregister(task: task)
265
+ }
266
+ }
@@ -429,6 +429,9 @@ public final class ReplayOrchestrator: NSObject {
429
429
 
430
430
  @objc public func logScreenView(_ screenId: String) {
431
431
  guard !screenId.isEmpty else { return }
432
+ if _visitedScreens.count >= 500 {
433
+ _visitedScreens.removeFirst(_visitedScreens.count - 250)
434
+ }
432
435
  _visitedScreens.append(screenId)
433
436
  currentScreenName = screenId
434
437
  if hierarchyCaptureEnabled { _captureHierarchy() }
@@ -44,17 +44,21 @@ final class SegmentDispatcher {
44
44
  }()
45
45
 
46
46
  private let httpSession: URLSession = {
47
- // Industry standard: Use ephemeral config with explicit connection limits
48
47
  let cfg = URLSessionConfiguration.ephemeral
49
48
  cfg.httpMaximumConnectionsPerHost = 4
50
49
  cfg.waitsForConnectivity = true
51
50
  cfg.timeoutIntervalForRequest = 30
52
51
  cfg.timeoutIntervalForResource = 60
52
+ // Strip our own protocol to prevent self-interception. Without this,
53
+ // every SDK upload is intercepted by RejourneyURLProtocol which
54
+ // generates redundant network events and wastes resources.
55
+ cfg.protocolClasses = cfg.protocolClasses?.filter { $0 != RejourneyURLProtocol.self } ?? []
53
56
  return URLSession(configuration: cfg)
54
57
  }()
55
58
 
56
59
  private var retryQueue: [PendingUpload] = []
57
60
  private let retryLock = NSLock()
61
+ private let maxRetryQueueSize = 20
58
62
  private var active = true
59
63
 
60
64
  private let metricsLock = NSLock()
@@ -332,6 +336,9 @@ final class SegmentDispatcher {
332
336
  var retry = upload
333
337
  retry.attempt += 1
334
338
  retryLock.lock()
339
+ if retryQueue.count >= maxRetryQueueSize {
340
+ retryQueue.removeFirst()
341
+ }
335
342
  retryQueue.append(retry)
336
343
  retryLock.unlock()
337
344
 
@@ -206,9 +206,16 @@ public final class VisualCapture: NSObject {
206
206
  // Refresh map detection state (very cheap shallow walk)
207
207
  SpecialCases.shared.refreshMapState()
208
208
 
209
- // Debug-only: confirm capture is running and map state
210
209
  if _frameCounter < 5 || _frameCounter % 30 == 0 {
211
- DiagnosticLog.trace("[VisualCapture] frame#\(_frameCounter) mapVisible=\(SpecialCases.shared.mapVisible) mapIdle=\(SpecialCases.shared.mapIdle) forced=\(forced)")
210
+ var info = mach_task_basic_info()
211
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
212
+ let _ = withUnsafeMutablePointer(to: &info) {
213
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
214
+ task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
215
+ }
216
+ }
217
+ let memMB = Double(info.resident_size) / 1_048_576.0
218
+ DiagnosticLog.trace("[VisualCapture] frame#\(_frameCounter) mapVisible=\(SpecialCases.shared.mapVisible) mapIdle=\(SpecialCases.shared.mapIdle) forced=\(forced) residentMB=\(String(format: "%.0f", memMB))")
212
219
  }
213
220
 
214
221
  // Map stutter prevention: when a map view is visible and its camera
package/ios/Rejourney.h CHANGED
@@ -24,10 +24,14 @@
24
24
  #import <ReactCommon/RCTTurboModule.h>
25
25
  #if __has_include(<RejourneySpec/RejourneySpec.h>)
26
26
  #import <RejourneySpec/RejourneySpec.h>
27
+ #define RJ_USE_NEW_ARCH_CODEGEN 1
27
28
  #elif __has_include("RejourneySpec.h")
28
29
  #import "RejourneySpec.h"
30
+ #define RJ_USE_NEW_ARCH_CODEGEN 1
31
+ #endif
29
32
  #endif
30
33
 
34
+ #if defined(RCT_NEW_ARCH_ENABLED) && defined(RJ_USE_NEW_ARCH_CODEGEN)
31
35
  @interface Rejourney : NSObject <NativeRejourneySpec>
32
36
  #else
33
37
  @interface Rejourney : NSObject <RCTBridgeModule>
package/ios/Rejourney.mm CHANGED
@@ -33,8 +33,10 @@
33
33
  #import <ReactCommon/RCTTurboModule.h>
34
34
  #if __has_include(<RejourneySpec/RejourneySpec.h>)
35
35
  #import <RejourneySpec/RejourneySpec.h>
36
+ #define RJ_USE_NEW_ARCH_CODEGEN 1
36
37
  #elif __has_include("RejourneySpec.h")
37
38
  #import "RejourneySpec.h"
39
+ #define RJ_USE_NEW_ARCH_CODEGEN 1
38
40
  #endif
39
41
  #endif
40
42
 
@@ -110,7 +112,7 @@ RCT_EXPORT_METHOD(removeListeners : (double)count) {
110
112
  return _impl;
111
113
  }
112
114
 
113
- #ifdef RCT_NEW_ARCH_ENABLED
115
+ #if defined(RCT_NEW_ARCH_ENABLED) && defined(RJ_USE_NEW_ARCH_CODEGEN)
114
116
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
115
117
  (const facebook::react::ObjCTurboModule::InitParams &)params {
116
118
  return std::make_shared<facebook::react::NativeRejourneySpecJSI>(params);
@@ -139,20 +141,6 @@ RCT_EXPORT_METHOD(startSession : (NSString *)userId apiUrl : (NSString *)
139
141
  reject:reject];
140
142
  }
141
143
 
142
- RCT_EXPORT_METHOD(startSessionWithOptions : (NSDictionary *)options resolve : (
143
- RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) {
144
- RejourneyImpl *impl = [self ensureImpl];
145
- if (!impl) {
146
- resolve(@{
147
- @"success" : @NO,
148
- @"sessionId" : @"",
149
- @"error" : @"Native module not available"
150
- });
151
- return;
152
- }
153
- [impl startSessionWithOptions:options resolve:resolve reject:reject];
154
- }
155
-
156
144
  RCT_EXPORT_METHOD(stopSession : (RCTPromiseResolveBlock)
157
145
  resolve reject : (RCTPromiseRejectBlock)reject) {
158
146
  RejourneyImpl *impl = [self ensureImpl];
@@ -902,6 +902,9 @@ function trackScreen(screenName) {
902
902
  }
903
903
  const previousScreen = currentScreen;
904
904
  currentScreen = screenName;
905
+ if (screensVisited.length >= 500) {
906
+ screensVisited.splice(0, screensVisited.length - 250);
907
+ }
905
908
  screensVisited.push(screenName);
906
909
  const uniqueScreens = new Set(screensVisited);
907
910
  metrics.uniqueScreensCount = uniqueScreens.size;
@@ -141,6 +141,9 @@ function incrementErrorCount() {
141
141
  metrics.totalEvents++;
142
142
  }
143
143
  function addScreenVisited(screenName) {
144
+ if (metrics.screensVisited.length >= 500) {
145
+ metrics.screensVisited.splice(0, metrics.screensVisited.length - 250);
146
+ }
144
147
  metrics.screensVisited.push(screenName);
145
148
  metrics.uniqueScreensCount = new Set(metrics.screensVisited).size;
146
149
  }
@@ -91,13 +91,10 @@ async function getFetchResponseSize(response) {
91
91
  const parsed = parseInt(contentLength, 10);
92
92
  if (Number.isFinite(parsed) && parsed > 0) return parsed;
93
93
  }
94
- try {
95
- const cloned = response.clone();
96
- const buffer = await cloned.arrayBuffer();
97
- return buffer.byteLength;
98
- } catch {
99
- return 0;
100
- }
94
+
95
+ // Don't clone+buffer the full body just to measure size when
96
+ // content-length is missing — this doubles memory for large responses.
97
+ return 0;
101
98
  }
102
99
  function getXhrResponseSize(xhr) {
103
100
  try {
@@ -347,6 +344,9 @@ function interceptXHR() {
347
344
  }
348
345
  data.t = Date.now();
349
346
  const onComplete = () => {
347
+ this.removeEventListener('load', onComplete);
348
+ this.removeEventListener('error', onComplete);
349
+ this.removeEventListener('abort', onComplete);
350
350
  const endTime = Date.now();
351
351
  const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
352
352
  queueRequest({
@@ -872,6 +872,9 @@ export function trackScreen(screenName) {
872
872
  }
873
873
  const previousScreen = currentScreen;
874
874
  currentScreen = screenName;
875
+ if (screensVisited.length >= 500) {
876
+ screensVisited.splice(0, screensVisited.length - 250);
877
+ }
875
878
  screensVisited.push(screenName);
876
879
  const uniqueScreens = new Set(screensVisited);
877
880
  metrics.uniqueScreensCount = uniqueScreens.size;
@@ -123,6 +123,9 @@ export function incrementErrorCount() {
123
123
  metrics.totalEvents++;
124
124
  }
125
125
  export function addScreenVisited(screenName) {
126
+ if (metrics.screensVisited.length >= 500) {
127
+ metrics.screensVisited.splice(0, metrics.screensVisited.length - 250);
128
+ }
126
129
  metrics.screensVisited.push(screenName);
127
130
  metrics.uniqueScreensCount = new Set(metrics.screensVisited).size;
128
131
  }
@@ -80,13 +80,10 @@ async function getFetchResponseSize(response) {
80
80
  const parsed = parseInt(contentLength, 10);
81
81
  if (Number.isFinite(parsed) && parsed > 0) return parsed;
82
82
  }
83
- try {
84
- const cloned = response.clone();
85
- const buffer = await cloned.arrayBuffer();
86
- return buffer.byteLength;
87
- } catch {
88
- return 0;
89
- }
83
+
84
+ // Don't clone+buffer the full body just to measure size when
85
+ // content-length is missing — this doubles memory for large responses.
86
+ return 0;
90
87
  }
91
88
  function getXhrResponseSize(xhr) {
92
89
  try {
@@ -336,6 +333,9 @@ function interceptXHR() {
336
333
  }
337
334
  data.t = Date.now();
338
335
  const onComplete = () => {
336
+ this.removeEventListener('load', onComplete);
337
+ this.removeEventListener('error', onComplete);
338
+ this.removeEventListener('abort', onComplete);
339
339
  const endTime = Date.now();
340
340
  const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
341
341
  queueRequest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejourneyco/react-native",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Rejourney Session Recording SDK for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
package/rejourney.podspec CHANGED
@@ -18,6 +18,15 @@ Pod::Spec.new do |s|
18
18
  s.exclude_files = "ios/build/**/*"
19
19
  s.library = "z"
20
20
 
21
- # Dependencies
22
- install_modules_dependencies(s)
21
+ # React Native core dependencies so headers like `React/RCTBridgeModule.h`
22
+ # are always available, regardless of React Native version or architecture.
23
+ # On modern React Native, `React-Core` is the canonical dependency.
24
+ s.dependency "React-Core"
25
+ s.dependency "ReactCommon/turbomodule/core"
26
+
27
+ # New Architecture / Codegen integration (RN 0.71+). On older RN versions
28
+ # this helper is not defined, so we guard it.
29
+ if respond_to?(:install_modules_dependencies)
30
+ install_modules_dependencies(s)
31
+ end
23
32
  end
@@ -1035,6 +1035,9 @@ export function trackScreen(screenName: string): void {
1035
1035
 
1036
1036
  const previousScreen = currentScreen;
1037
1037
  currentScreen = screenName;
1038
+ if (screensVisited.length >= 500) {
1039
+ screensVisited.splice(0, screensVisited.length - 250);
1040
+ }
1038
1041
  screensVisited.push(screenName);
1039
1042
 
1040
1043
  const uniqueScreens = new Set(screensVisited);
@@ -153,6 +153,9 @@ export function incrementErrorCount(): void {
153
153
  }
154
154
 
155
155
  export function addScreenVisited(screenName: string): void {
156
+ if (metrics.screensVisited.length >= 500) {
157
+ metrics.screensVisited.splice(0, metrics.screensVisited.length - 250);
158
+ }
156
159
  metrics.screensVisited.push(screenName);
157
160
  metrics.uniqueScreensCount = new Set(metrics.screensVisited).size;
158
161
  }
@@ -98,13 +98,9 @@ async function getFetchResponseSize(response: Response): Promise<number> {
98
98
  if (Number.isFinite(parsed) && parsed > 0) return parsed;
99
99
  }
100
100
 
101
- try {
102
- const cloned = response.clone();
103
- const buffer = await cloned.arrayBuffer();
104
- return buffer.byteLength;
105
- } catch {
106
- return 0;
107
- }
101
+ // Don't clone+buffer the full body just to measure size when
102
+ // content-length is missing — this doubles memory for large responses.
103
+ return 0;
108
104
  }
109
105
 
110
106
  function getXhrResponseSize(xhr: XMLHttpRequest): number {
@@ -403,6 +399,10 @@ function interceptXHR(): void {
403
399
  data.t = Date.now();
404
400
 
405
401
  const onComplete = () => {
402
+ this.removeEventListener('load', onComplete);
403
+ this.removeEventListener('error', onComplete);
404
+ this.removeEventListener('abort', onComplete);
405
+
406
406
  const endTime = Date.now();
407
407
 
408
408
  const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;