@luciq/react-native 19.2.1 → 19.3.0-40271-SNAPSHOT

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/CHANGELOG.md +30 -0
  2. package/README.md +87 -0
  3. package/RNLuciq.podspec +1 -1
  4. package/android/native.gradle +1 -1
  5. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +211 -117
  6. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  7. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +51 -9
  8. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  9. package/dist/constants/Strings.d.ts +9 -0
  10. package/dist/constants/Strings.js +12 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +2 -1
  13. package/dist/models/CustomSpan.d.ts +47 -0
  14. package/dist/models/CustomSpan.js +82 -0
  15. package/dist/modules/APM.d.ts +58 -0
  16. package/dist/modules/APM.js +62 -0
  17. package/dist/modules/Luciq.js +2 -1
  18. package/dist/modules/NetworkLogger.d.ts +0 -5
  19. package/dist/modules/NetworkLogger.js +9 -1
  20. package/dist/native/NativeAPM.d.ts +3 -0
  21. package/dist/native/NativeLuciq.d.ts +1 -0
  22. package/dist/utils/CustomSpansManager.d.ts +38 -0
  23. package/dist/utils/CustomSpansManager.js +173 -0
  24. package/dist/utils/FeatureFlags.d.ts +6 -0
  25. package/dist/utils/FeatureFlags.js +35 -0
  26. package/dist/utils/LuciqUtils.js +6 -0
  27. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  28. package/ios/RNLuciq/LuciqAPMBridge.h +13 -0
  29. package/ios/RNLuciq/LuciqAPMBridge.m +55 -0
  30. package/ios/RNLuciq/LuciqReactBridge.m +12 -0
  31. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +1 -0
  32. package/ios/native.rb +1 -1
  33. package/package.json +1 -2
  34. package/plugin/build/index.js +9 -2
  35. package/plugin/src/withLuciqIOS.ts +9 -2
  36. package/scripts/releases/changelog_to_slack_formatter.sh +9 -0
  37. package/scripts/releases/get_job_approver.sh +60 -0
  38. package/scripts/releases/get_release_notes.sh +22 -0
  39. package/scripts/releases/get_sdk_version.sh +5 -0
  40. package/scripts/releases/get_slack_id_from_username.sh +24 -0
  41. package/src/constants/Strings.ts +24 -0
  42. package/src/index.ts +2 -0
  43. package/src/models/CustomSpan.ts +102 -0
  44. package/src/modules/APM.ts +72 -0
  45. package/src/modules/Luciq.ts +3 -1
  46. package/src/modules/NetworkLogger.ts +26 -1
  47. package/src/native/NativeAPM.ts +7 -0
  48. package/src/native/NativeLuciq.ts +1 -0
  49. package/src/utils/CustomSpansManager.ts +202 -0
  50. package/src/utils/FeatureFlags.ts +44 -0
  51. package/src/utils/LuciqUtils.ts +15 -0
  52. package/src/utils/XhrNetworkInterceptor.ts +128 -55
@@ -0,0 +1,202 @@
1
+ import { NativeAPM } from '../native/NativeAPM';
2
+ import { NativeLuciq } from '../native/NativeLuciq';
3
+ import { CustomSpan } from '../models/CustomSpan';
4
+ import { LuciqStrings } from '../constants/Strings';
5
+
6
+ /**
7
+ * Tracks currently active custom spans
8
+ * @internal
9
+ */
10
+ const activeSpans = new Set<CustomSpan>();
11
+
12
+ /**
13
+ * Maximum concurrent custom spans allowed at any time
14
+ * @internal
15
+ */
16
+ const MAX_CONCURRENT_SPANS = 100;
17
+
18
+ /**
19
+ * Internal: unregister a span from active tracking
20
+ * @internal
21
+ */
22
+ const unregisterSpan = (span: CustomSpan): void => {
23
+ activeSpans.delete(span);
24
+ };
25
+
26
+ /**
27
+ * Internal: sync custom span data to native SDK
28
+ * @internal
29
+ */
30
+ const syncCustomSpan = async (
31
+ name: string,
32
+ startTimestamp: number,
33
+ endTimestamp: number,
34
+ ): Promise<void> => {
35
+ // Validate inputs (safety net)
36
+ if (!name || name.trim().length === 0) {
37
+ console.error(LuciqStrings.customSpanNameEmpty);
38
+ return;
39
+ }
40
+
41
+ if (endTimestamp <= startTimestamp) {
42
+ console.error(LuciqStrings.customSpanEndTimeBeforeStartTime);
43
+ return;
44
+ }
45
+
46
+ // Truncate name if needed (safety net)
47
+ let spanName = name.trim();
48
+ if (spanName.length > 150) {
49
+ spanName = spanName.substring(0, 150);
50
+ }
51
+
52
+ await NativeAPM.syncCustomSpan(spanName, startTimestamp, endTimestamp);
53
+ };
54
+
55
+ /**
56
+ * Starts a custom span for performance tracking.
57
+ *
58
+ * A custom span measures the duration of an arbitrary operation that is not
59
+ * automatically tracked by the SDK. The span must be manually ended by calling
60
+ * the `end()` method on the returned span object.
61
+ *
62
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
63
+ * Leading and trailing whitespace will be trimmed.
64
+ *
65
+ * @returns Promise<CustomSpan | null> - The span object to end later, or null if:
66
+ * - Name is empty after trimming
67
+ * - SDK is not initialized
68
+ * - APM is disabled
69
+ * - Custom spans feature is disabled
70
+ * - Maximum concurrent spans limit (100) reached
71
+ */
72
+ export const startCustomSpan = async (name: string): Promise<CustomSpan | null> => {
73
+ try {
74
+ // Validate name
75
+ const trimmedName = name.trim();
76
+ if (trimmedName.length === 0) {
77
+ console.error(LuciqStrings.customSpanNameEmpty);
78
+ return null;
79
+ }
80
+
81
+ // Check SDK initialization
82
+ const isInitialized = await NativeLuciq.isBuilt();
83
+ if (!isInitialized) {
84
+ console.error(LuciqStrings.customSpanSDKNotInitializedMessage);
85
+ return null;
86
+ }
87
+
88
+ // Check APM enabled
89
+ const isAPMEnabled = await NativeAPM.isAPMEnabled();
90
+ if (!isAPMEnabled) {
91
+ console.log(LuciqStrings.customSpanAPMDisabledMessage);
92
+ return null;
93
+ }
94
+
95
+ // Check custom spans enabled
96
+ const isCustomSpanEnabled = await NativeAPM.isCustomSpanEnabled();
97
+ if (!isCustomSpanEnabled) {
98
+ console.log(LuciqStrings.customSpanDisabled);
99
+ return null;
100
+ }
101
+
102
+ // Check concurrent span limit
103
+ if (activeSpans.size >= MAX_CONCURRENT_SPANS) {
104
+ console.error(LuciqStrings.customSpanLimitReached);
105
+ return null;
106
+ }
107
+
108
+ // Truncate name if needed
109
+ let spanName = trimmedName;
110
+ if (spanName.length > 150) {
111
+ spanName = spanName.substring(0, 150);
112
+ console.log(LuciqStrings.customSpanNameTruncated);
113
+ }
114
+
115
+ // Create and register span with callbacks
116
+ const span = new CustomSpan(spanName, unregisterSpan, syncCustomSpan);
117
+ activeSpans.add(span);
118
+ return span;
119
+ } catch (error) {
120
+ console.error('[CustomSpan] Error starting span:', error);
121
+ return null;
122
+ }
123
+ };
124
+
125
+ /**
126
+ * Records a completed custom span with pre-recorded timestamps.
127
+ *
128
+ * Use this method when you have already recorded the start and end times
129
+ * of an operation and want to report it retroactively.
130
+ *
131
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
132
+ * Leading and trailing whitespace will be trimmed.
133
+ * @param startDate - The start time of the operation
134
+ * @param endDate - The end time of the operation (must be after startDate)
135
+ *
136
+ * @returns Promise<void>
137
+ */
138
+ export const addCompletedCustomSpan = async (
139
+ name: string,
140
+ startDate: Date,
141
+ endDate: Date,
142
+ ): Promise<void> => {
143
+ try {
144
+ // Validate name
145
+ const trimmedName = name.trim();
146
+ if (trimmedName.length === 0) {
147
+ console.error(LuciqStrings.customSpanNameEmpty);
148
+ return;
149
+ }
150
+
151
+ // Validate timestamps
152
+ if (endDate <= startDate) {
153
+ console.error(LuciqStrings.customSpanEndTimeBeforeStartTime);
154
+ return;
155
+ }
156
+
157
+ // Check SDK initialization
158
+ const isInitialized = await NativeLuciq.isBuilt();
159
+ if (!isInitialized) {
160
+ console.error(LuciqStrings.customSpanSDKNotInitializedMessage);
161
+ return;
162
+ }
163
+
164
+ // Check APM enabled
165
+ const isAPMEnabled = await NativeAPM.isAPMEnabled();
166
+ if (!isAPMEnabled) {
167
+ console.log(LuciqStrings.customSpanAPMDisabledMessage);
168
+ return;
169
+ }
170
+
171
+ // Check custom spans enabled
172
+ const isCustomSpanEnabled = await NativeAPM.isCustomSpanEnabled();
173
+ if (!isCustomSpanEnabled) {
174
+ console.log(LuciqStrings.customSpanDisabled);
175
+ return;
176
+ }
177
+
178
+ // Truncate name if needed
179
+ let spanName = trimmedName;
180
+ if (spanName.length > 150) {
181
+ spanName = spanName.substring(0, 150);
182
+ console.log(LuciqStrings.customSpanNameTruncated);
183
+ }
184
+
185
+ // Convert to microseconds
186
+ const startMicros = startDate.getTime() * 1000;
187
+ const endMicros = endDate.getTime() * 1000;
188
+
189
+ // Send to native SDK
190
+ await syncCustomSpan(spanName, startMicros, endMicros);
191
+ } catch (error) {
192
+ console.error('[CustomSpan] Error adding completed span:', error);
193
+ }
194
+ };
195
+
196
+ /**
197
+ * Test-only helper to clear active spans between tests.
198
+ * @internal
199
+ */
200
+ export const __resetCustomSpansForTests = (): void => {
201
+ activeSpans.clear();
202
+ };
@@ -1,5 +1,40 @@
1
1
  import { NativeLuciq } from '../native/NativeLuciq';
2
2
  import { _registerFeatureFlagsChangeListener } from '../modules/Luciq';
3
+ import { Logger } from './logger';
4
+
5
+ const TAG = 'LCQ-RN-NET:';
6
+
7
+ let cachedW3cFlags = {
8
+ isW3cExternalTraceIDEnabled: false,
9
+ isW3cExternalGeneratedHeaderEnabled: false,
10
+ isW3cCaughtHeaderEnabled: false,
11
+ };
12
+
13
+ export async function initFeatureFlagsCache() {
14
+ Logger.debug(TAG, '[FeatureFlags] Initializing W3C feature flags cache from native bridge...');
15
+ try {
16
+ const [traceID, generatedHeader, caughtHeader] = await Promise.all([
17
+ NativeLuciq.isW3ExternalTraceIDEnabled(),
18
+ NativeLuciq.isW3ExternalGeneratedHeaderEnabled(),
19
+ NativeLuciq.isW3CaughtHeaderEnabled(),
20
+ ]);
21
+ cachedW3cFlags = {
22
+ isW3cExternalTraceIDEnabled: traceID,
23
+ isW3cExternalGeneratedHeaderEnabled: generatedHeader,
24
+ isW3cCaughtHeaderEnabled: caughtHeader,
25
+ };
26
+ Logger.debug(
27
+ TAG,
28
+ `[FeatureFlags] Cache initialized: traceID=${traceID}, generatedHeader=${generatedHeader}, caughtHeader=${caughtHeader}`,
29
+ );
30
+ } catch (e) {
31
+ Logger.debug(TAG, '[FeatureFlags] Failed to initialize cache, using defaults (all false):', e);
32
+ }
33
+ }
34
+
35
+ export function getCachedW3cFlags() {
36
+ return cachedW3cFlags;
37
+ }
3
38
 
4
39
  export const FeatureFlags = {
5
40
  isW3ExternalTraceID: () => NativeLuciq.isW3ExternalTraceIDEnabled(),
@@ -16,6 +51,15 @@ export const registerFeatureFlagsListener = () => {
16
51
  isW3CaughtHeaderEnabled: boolean;
17
52
  networkBodyLimit: number;
18
53
  }) => {
54
+ Logger.debug(
55
+ TAG,
56
+ `[FeatureFlags] Flags updated from native listener: traceID=${res.isW3ExternalTraceIDEnabled}, generatedHeader=${res.isW3ExternalGeneratedHeaderEnabled}, caughtHeader=${res.isW3CaughtHeaderEnabled}, bodyLimit=${res.networkBodyLimit}`,
57
+ );
58
+ cachedW3cFlags = {
59
+ isW3cExternalTraceIDEnabled: res.isW3ExternalTraceIDEnabled,
60
+ isW3cExternalGeneratedHeaderEnabled: res.isW3ExternalGeneratedHeaderEnabled,
61
+ isW3cCaughtHeaderEnabled: res.isW3CaughtHeaderEnabled,
62
+ };
19
63
  FeatureFlags.isW3ExternalTraceID = async () => {
20
64
  return res.isW3ExternalTraceIDEnabled;
21
65
  };
@@ -12,6 +12,7 @@ import { NativeCrashReporting } from '../native/NativeCrashReporting';
12
12
  import type { NetworkData } from './XhrNetworkInterceptor';
13
13
  import { NativeLuciq } from '../native/NativeLuciq';
14
14
  import { NativeAPM } from '../native/NativeAPM';
15
+ import { Logger } from './logger';
15
16
  import * as NetworkLogger from '../modules/NetworkLogger';
16
17
  import {
17
18
  NativeNetworkLogger,
@@ -231,6 +232,11 @@ export const reportNetworkLog = (network: NetworkData) => {
231
232
  const requestHeaders = JSON.stringify(network.requestHeaders);
232
233
  const responseHeaders = JSON.stringify(network.responseHeaders);
233
234
 
235
+ Logger.debug(
236
+ 'LCQ-RN-NET:',
237
+ `[reportNetworkLog] Sending to NativeLuciq.networkLogAndroid: ${network.method} ${network.url}, status=${network.responseCode}, duration=${network.duration}ms, error=${network.errorDomain || 'none'}`,
238
+ );
239
+
234
240
  NativeLuciq.networkLogAndroid(
235
241
  network.url,
236
242
  network.requestBody,
@@ -246,6 +252,10 @@ export const reportNetworkLog = (network: NetworkData) => {
246
252
  !apmFlags.hasAPMNetworkPlugin ||
247
253
  !apmFlags.shouldEnableNativeInterception
248
254
  ) {
255
+ Logger.debug(
256
+ 'LCQ-RN-NET:',
257
+ `[reportNetworkLog] Also sending to NativeAPM.networkLogAndroid (native interception disabled): ${network.method} ${network.url}`,
258
+ );
249
259
  NativeAPM.networkLogAndroid(
250
260
  network.startTime,
251
261
  network.duration,
@@ -271,6 +281,11 @@ export const reportNetworkLog = (network: NetworkData) => {
271
281
  network.gqlQueryName,
272
282
  network.serverErrorMessage,
273
283
  );
284
+ } else {
285
+ Logger.debug(
286
+ 'LCQ-RN-NET:',
287
+ `[reportNetworkLog] Skipping NativeAPM.networkLogAndroid (native interception enabled): nativeFeature=${apmFlags.isNativeInterceptionFeatureEnabled}, hasPlugin=${apmFlags.hasAPMNetworkPlugin}, shouldEnable=${apmFlags.shouldEnableNativeInterception}`,
288
+ );
274
289
  }
275
290
  } else {
276
291
  NativeLuciq.networkLogIOS(
@@ -1,7 +1,10 @@
1
1
  import LuciqConstants from './LuciqConstants';
2
2
  import { stringifyIfNotString, generateW3CHeader } from './LuciqUtils';
3
3
 
4
- import { FeatureFlags } from '../utils/FeatureFlags';
4
+ import { getCachedW3cFlags } from './FeatureFlags';
5
+ import { Logger } from './logger';
6
+
7
+ const TAG = 'LCQ-RN-NET:';
5
8
 
6
9
  export type ProgressCallback = (totalBytesSent: number, totalBytesExpectedToSend: number) => void;
7
10
  export type NetworkDataCallback = (data: NetworkData) => void;
@@ -40,45 +43,41 @@ let originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
40
43
  let onProgressCallback: ProgressCallback | null;
41
44
  let onDoneCallback: NetworkDataCallback | null;
42
45
  let isInterceptorEnabled = false;
43
- let network: NetworkData;
44
-
45
- const _reset = () => {
46
- network = {
47
- id: '',
48
- url: '',
49
- method: '',
50
- requestBody: '',
51
- requestBodySize: 0,
52
- responseBody: '',
53
- responseBodySize: 0,
54
- responseCode: 0,
55
- requestHeaders: {},
56
- responseHeaders: {},
57
- contentType: '',
58
- errorDomain: '',
59
- errorCode: 0,
60
- startTime: 0,
61
- duration: 0,
62
- gqlQueryName: '',
63
- serverErrorMessage: '',
64
- requestContentType: '',
65
- isW3cHeaderFound: null,
66
- partialId: null,
67
- networkStartTimeInSeconds: null,
68
- w3cGeneratedHeader: null,
69
- w3cCaughtHeader: null,
70
- };
71
- };
72
- const getTraceparentHeader = async (networkData: NetworkData) => {
73
- const [
46
+
47
+ const networkMap = new WeakMap<XMLHttpRequest, NetworkData>();
48
+
49
+ const createNetworkData = (): NetworkData => ({
50
+ id: '',
51
+ url: '',
52
+ method: '',
53
+ requestBody: '',
54
+ requestBodySize: 0,
55
+ responseBody: '',
56
+ responseBodySize: 0,
57
+ responseCode: 0,
58
+ requestHeaders: {},
59
+ responseHeaders: {},
60
+ contentType: '',
61
+ errorDomain: '',
62
+ errorCode: 0,
63
+ startTime: 0,
64
+ duration: 0,
65
+ gqlQueryName: '',
66
+ serverErrorMessage: '',
67
+ requestContentType: '',
68
+ isW3cHeaderFound: null,
69
+ partialId: null,
70
+ networkStartTimeInSeconds: null,
71
+ w3cGeneratedHeader: null,
72
+ w3cCaughtHeader: null,
73
+ });
74
+
75
+ const getTraceparentHeader = (networkData: NetworkData) => {
76
+ const {
74
77
  isW3cExternalTraceIDEnabled,
75
78
  isW3cExternalGeneratedHeaderEnabled,
76
79
  isW3cCaughtHeaderEnabled,
77
- ] = await Promise.all([
78
- FeatureFlags.isW3ExternalTraceID(),
79
- FeatureFlags.isW3ExternalGeneratedHeader(),
80
- FeatureFlags.isW3CaughtHeader(),
81
- ]);
80
+ } = getCachedW3cFlags();
82
81
 
83
82
  return injectHeaders(networkData, {
84
83
  isW3cExternalTraceIDEnabled,
@@ -147,44 +146,77 @@ export default {
147
146
  onProgressCallback = callback;
148
147
  },
149
148
  enableInterception() {
150
- // Prevents infinite calls to XMLHttpRequest.open when enabling interception multiple times
151
149
  if (isInterceptorEnabled) {
150
+ Logger.debug(TAG, 'enableInterception called but already enabled, skipping');
152
151
  return;
153
152
  }
154
153
 
154
+ Logger.debug(TAG, 'Enabling XHR network interception');
155
+
155
156
  originalXHROpen = XMLHttpRequest.prototype.open;
156
157
  originalXHRSend = XMLHttpRequest.prototype.send;
157
158
  originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
158
159
  // An error code that signifies an issue with the RN client.
159
160
  const clientErrorCode = 9876;
160
161
  XMLHttpRequest.prototype.open = function (method, url, ...args) {
161
- _reset();
162
- network.url = url;
163
- network.method = method;
162
+ const networkData = createNetworkData();
163
+ networkData.url = url;
164
+ networkData.method = method;
165
+ networkMap.set(this, networkData);
166
+ Logger.debug(TAG, `[open] ${method} ${url}`);
164
167
  originalXHROpen.apply(this, [method, url, ...args]);
165
168
  };
166
169
 
167
170
  XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
168
- // According to the HTTP RFC, headers are case-insensitive, so we convert
169
- // them to lower-case to make accessing headers predictable.
170
- // This avoid issues like failing to get the Content-Type header for a request
171
- // because the header is set as 'Content-Type' instead of 'content-type'.
172
171
  const key = header.toLowerCase();
173
- network.requestHeaders[key] = stringifyIfNotString(value);
172
+ const networkData = networkMap.get(this);
173
+ if (networkData) {
174
+ networkData.requestHeaders[key] = stringifyIfNotString(value);
175
+ } else {
176
+ Logger.debug(
177
+ TAG,
178
+ `[setRequestHeader] No networkData found in WeakMap for header "${key}" — request may have been GC'd or open() was not called`,
179
+ );
180
+ }
174
181
  originalXHRSetRequestHeader.apply(this, [header, value]);
175
182
  };
176
183
 
177
- XMLHttpRequest.prototype.send = async function (data) {
178
- const cloneNetwork = JSON.parse(JSON.stringify(network));
184
+ XMLHttpRequest.prototype.send = function (data) {
185
+ const networkData = networkMap.get(this);
186
+ if (!networkData) {
187
+ Logger.debug(
188
+ TAG,
189
+ '[send] No networkData found in WeakMap — falling back to original send (open() was not intercepted)',
190
+ );
191
+ originalXHRSend.apply(this, [data]);
192
+ return;
193
+ }
194
+
195
+ Logger.debug(TAG, `[send] ${networkData.method} ${networkData.url}`);
196
+
197
+ const cloneNetwork = JSON.parse(JSON.stringify(networkData));
179
198
  cloneNetwork.requestBody = data ? data : '';
180
199
 
181
200
  if (typeof cloneNetwork.requestBody !== 'string') {
182
201
  cloneNetwork.requestBody = JSON.stringify(cloneNetwork.requestBody);
183
202
  }
184
203
 
204
+ let isReported = false;
205
+
185
206
  if (this.addEventListener) {
186
207
  this.addEventListener('readystatechange', async () => {
187
208
  if (!isInterceptorEnabled) {
209
+ Logger.debug(
210
+ TAG,
211
+ `[readystatechange] Interceptor disabled, ignoring state=${this.readyState} for ${cloneNetwork.url}`,
212
+ );
213
+ return;
214
+ }
215
+ if (isReported) {
216
+ Logger.debug(
217
+ TAG,
218
+ `[readystatechange] Already reported, ignoring state=${this.readyState} for ${cloneNetwork.url}`,
219
+ );
188
220
  return;
189
221
  }
190
222
  if (this.readyState === this.HEADERS_RECEIVED) {
@@ -217,6 +249,11 @@ export default {
217
249
  cloneNetwork.requestContentType =
218
250
  cloneNetwork.requestHeaders['content-type'].split(';')[0];
219
251
  }
252
+
253
+ Logger.debug(
254
+ TAG,
255
+ `[readystatechange] HEADERS_RECEIVED for ${cloneNetwork.url}, contentType=${cloneNetwork.contentType}`,
256
+ );
220
257
  }
221
258
 
222
259
  if (this.readyState === this.DONE) {
@@ -239,12 +276,15 @@ export default {
239
276
  typeof _response === 'string' ? _response : JSON.stringify(_response);
240
277
  cloneNetwork.responseBody = '';
241
278
 
242
- // Detect a more descriptive error message.
243
279
  if (typeof _response === 'string' && _response.length > 0) {
244
280
  cloneNetwork.errorDomain = _response;
245
281
  }
246
282
 
247
283
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
284
+ Logger.debug(
285
+ TAG,
286
+ `[readystatechange] DONE with client error for ${cloneNetwork.url}, errorDomain=${cloneNetwork.errorDomain}`,
287
+ );
248
288
 
249
289
  // @ts-ignore
250
290
  } else if (this._timedOut) {
@@ -253,6 +293,7 @@ export default {
253
293
  cloneNetwork.responseCode = 0;
254
294
  cloneNetwork.contentType = 'text/plain';
255
295
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
296
+ Logger.debug(TAG, `[readystatechange] DONE with timeout for ${cloneNetwork.url}`);
256
297
  }
257
298
 
258
299
  // Only set response body if not already set by error handlers
@@ -300,8 +341,18 @@ export default {
300
341
  delete cloneNetwork.gqlQueryName;
301
342
  }
302
343
 
344
+ isReported = true;
345
+ Logger.debug(
346
+ TAG,
347
+ `[readystatechange] DONE for ${cloneNetwork.method} ${cloneNetwork.url} — status=${cloneNetwork.responseCode}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`,
348
+ );
303
349
  if (onDoneCallback) {
304
350
  onDoneCallback(cloneNetwork);
351
+ } else {
352
+ Logger.debug(
353
+ TAG,
354
+ `[readystatechange] WARNING: onDoneCallback is null, network log for ${cloneNetwork.url} will be LOST`,
355
+ );
305
356
  }
306
357
  }
307
358
  });
@@ -310,7 +361,6 @@ export default {
310
361
  if (!isInterceptorEnabled) {
311
362
  return;
312
363
  }
313
- // check if will be able to compute progress
314
364
  if (event.lengthComputable && onProgressCallback) {
315
365
  const totalBytesSent = event.loaded;
316
366
  const totalBytesExpectedToSend = event.total - event.loaded;
@@ -320,34 +370,57 @@ export default {
320
370
  this.addEventListener('progress', downloadUploadProgressCallback);
321
371
  this.upload.addEventListener('progress', downloadUploadProgressCallback);
322
372
 
323
- // Handler for abort events (works with fetch, Axios, and any XHR-based requests)
324
373
  this.addEventListener('abort', () => {
325
374
  if (!isInterceptorEnabled) {
375
+ Logger.debug(
376
+ TAG,
377
+ `[abort] Interceptor disabled, ignoring abort for ${cloneNetwork.url}`,
378
+ );
326
379
  return;
327
380
  }
381
+ if (isReported) {
382
+ Logger.debug(
383
+ TAG,
384
+ `[abort] Already reported via readystatechange DONE, ignoring duplicate abort for ${cloneNetwork.url}`,
385
+ );
386
+ return;
387
+ }
388
+ isReported = true;
328
389
  cloneNetwork.duration = Date.now() - cloneNetwork.startTime;
329
390
  cloneNetwork.responseCode = 0;
330
391
  cloneNetwork.errorCode = clientErrorCode;
331
392
  cloneNetwork.errorDomain = 'cancelled';
332
393
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
394
+ Logger.debug(
395
+ TAG,
396
+ `[abort] Request cancelled: ${cloneNetwork.method} ${cloneNetwork.url}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`,
397
+ );
398
+ if (onDoneCallback) {
399
+ onDoneCallback(cloneNetwork);
400
+ } else {
401
+ Logger.debug(
402
+ TAG,
403
+ `[abort] WARNING: onDoneCallback is null, cancelled log for ${cloneNetwork.url} will be LOST`,
404
+ );
405
+ }
333
406
  });
334
407
  }
335
408
 
336
409
  cloneNetwork.startTime = Date.now();
337
- const traceparent = await getTraceparentHeader(cloneNetwork);
410
+ const traceparent = getTraceparentHeader(cloneNetwork);
338
411
  if (traceparent) {
339
412
  this.setRequestHeader('Traceparent', traceparent);
340
- }
341
- if (this.readyState === this.UNSENT) {
342
- return; // Prevent sending the request if not opened
413
+ Logger.debug(TAG, `[send] Injected traceparent header for ${cloneNetwork.url}`);
343
414
  }
344
415
 
345
416
  originalXHRSend.apply(this, [data]);
346
417
  };
347
418
  isInterceptorEnabled = true;
419
+ Logger.debug(TAG, 'XHR network interception enabled');
348
420
  },
349
421
 
350
422
  disableInterception() {
423
+ Logger.debug(TAG, 'Disabling XHR network interception');
351
424
  isInterceptorEnabled = false;
352
425
  XMLHttpRequest.prototype.send = originalXHRSend;
353
426
  XMLHttpRequest.prototype.open = originalXHROpen;