@luciq/react-native 19.2.2 → 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 (49) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +87 -0
  3. package/android/native.gradle +1 -1
  4. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +211 -117
  5. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  6. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +51 -9
  7. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  8. package/dist/constants/Strings.d.ts +9 -0
  9. package/dist/constants/Strings.js +12 -0
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +2 -1
  12. package/dist/models/CustomSpan.d.ts +47 -0
  13. package/dist/models/CustomSpan.js +82 -0
  14. package/dist/modules/APM.d.ts +58 -0
  15. package/dist/modules/APM.js +62 -0
  16. package/dist/modules/Luciq.js +2 -1
  17. package/dist/modules/NetworkLogger.d.ts +0 -5
  18. package/dist/modules/NetworkLogger.js +9 -1
  19. package/dist/native/NativeAPM.d.ts +3 -0
  20. package/dist/native/NativeLuciq.d.ts +1 -0
  21. package/dist/utils/CustomSpansManager.d.ts +38 -0
  22. package/dist/utils/CustomSpansManager.js +173 -0
  23. package/dist/utils/FeatureFlags.d.ts +6 -0
  24. package/dist/utils/FeatureFlags.js +35 -0
  25. package/dist/utils/LuciqUtils.js +6 -0
  26. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  27. package/ios/RNLuciq/LuciqAPMBridge.h +13 -0
  28. package/ios/RNLuciq/LuciqAPMBridge.m +55 -0
  29. package/ios/RNLuciq/LuciqReactBridge.m +12 -0
  30. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +1 -0
  31. package/ios/native.rb +1 -1
  32. package/package.json +1 -2
  33. package/scripts/releases/changelog_to_slack_formatter.sh +9 -0
  34. package/scripts/releases/get_job_approver.sh +60 -0
  35. package/scripts/releases/get_release_notes.sh +22 -0
  36. package/scripts/releases/get_sdk_version.sh +5 -0
  37. package/scripts/releases/get_slack_id_from_username.sh +24 -0
  38. package/src/constants/Strings.ts +24 -0
  39. package/src/index.ts +2 -0
  40. package/src/models/CustomSpan.ts +102 -0
  41. package/src/modules/APM.ts +72 -0
  42. package/src/modules/Luciq.ts +3 -1
  43. package/src/modules/NetworkLogger.ts +26 -1
  44. package/src/native/NativeAPM.ts +7 -0
  45. package/src/native/NativeLuciq.ts +1 -0
  46. package/src/utils/CustomSpansManager.ts +202 -0
  47. package/src/utils/FeatureFlags.ts +44 -0
  48. package/src/utils/LuciqUtils.ts +15 -0
  49. package/src/utils/XhrNetworkInterceptor.ts +128 -55
@@ -0,0 +1,173 @@
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
+ * Tracks currently active custom spans
7
+ * @internal
8
+ */
9
+ const activeSpans = new Set();
10
+ /**
11
+ * Maximum concurrent custom spans allowed at any time
12
+ * @internal
13
+ */
14
+ const MAX_CONCURRENT_SPANS = 100;
15
+ /**
16
+ * Internal: unregister a span from active tracking
17
+ * @internal
18
+ */
19
+ const unregisterSpan = (span) => {
20
+ activeSpans.delete(span);
21
+ };
22
+ /**
23
+ * Internal: sync custom span data to native SDK
24
+ * @internal
25
+ */
26
+ const syncCustomSpan = async (name, startTimestamp, endTimestamp) => {
27
+ // Validate inputs (safety net)
28
+ if (!name || name.trim().length === 0) {
29
+ console.error(LuciqStrings.customSpanNameEmpty);
30
+ return;
31
+ }
32
+ if (endTimestamp <= startTimestamp) {
33
+ console.error(LuciqStrings.customSpanEndTimeBeforeStartTime);
34
+ return;
35
+ }
36
+ // Truncate name if needed (safety net)
37
+ let spanName = name.trim();
38
+ if (spanName.length > 150) {
39
+ spanName = spanName.substring(0, 150);
40
+ }
41
+ await NativeAPM.syncCustomSpan(spanName, startTimestamp, endTimestamp);
42
+ };
43
+ /**
44
+ * Starts a custom span for performance tracking.
45
+ *
46
+ * A custom span measures the duration of an arbitrary operation that is not
47
+ * automatically tracked by the SDK. The span must be manually ended by calling
48
+ * the `end()` method on the returned span object.
49
+ *
50
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
51
+ * Leading and trailing whitespace will be trimmed.
52
+ *
53
+ * @returns Promise<CustomSpan | null> - The span object to end later, or null if:
54
+ * - Name is empty after trimming
55
+ * - SDK is not initialized
56
+ * - APM is disabled
57
+ * - Custom spans feature is disabled
58
+ * - Maximum concurrent spans limit (100) reached
59
+ */
60
+ export const startCustomSpan = async (name) => {
61
+ try {
62
+ // Validate name
63
+ const trimmedName = name.trim();
64
+ if (trimmedName.length === 0) {
65
+ console.error(LuciqStrings.customSpanNameEmpty);
66
+ return null;
67
+ }
68
+ // Check SDK initialization
69
+ const isInitialized = await NativeLuciq.isBuilt();
70
+ if (!isInitialized) {
71
+ console.error(LuciqStrings.customSpanSDKNotInitializedMessage);
72
+ return null;
73
+ }
74
+ // Check APM enabled
75
+ const isAPMEnabled = await NativeAPM.isAPMEnabled();
76
+ if (!isAPMEnabled) {
77
+ console.log(LuciqStrings.customSpanAPMDisabledMessage);
78
+ return null;
79
+ }
80
+ // Check custom spans enabled
81
+ const isCustomSpanEnabled = await NativeAPM.isCustomSpanEnabled();
82
+ if (!isCustomSpanEnabled) {
83
+ console.log(LuciqStrings.customSpanDisabled);
84
+ return null;
85
+ }
86
+ // Check concurrent span limit
87
+ if (activeSpans.size >= MAX_CONCURRENT_SPANS) {
88
+ console.error(LuciqStrings.customSpanLimitReached);
89
+ return null;
90
+ }
91
+ // Truncate name if needed
92
+ let spanName = trimmedName;
93
+ if (spanName.length > 150) {
94
+ spanName = spanName.substring(0, 150);
95
+ console.log(LuciqStrings.customSpanNameTruncated);
96
+ }
97
+ // Create and register span with callbacks
98
+ const span = new CustomSpan(spanName, unregisterSpan, syncCustomSpan);
99
+ activeSpans.add(span);
100
+ return span;
101
+ }
102
+ catch (error) {
103
+ console.error('[CustomSpan] Error starting span:', error);
104
+ return null;
105
+ }
106
+ };
107
+ /**
108
+ * Records a completed custom span with pre-recorded timestamps.
109
+ *
110
+ * Use this method when you have already recorded the start and end times
111
+ * of an operation and want to report it retroactively.
112
+ *
113
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
114
+ * Leading and trailing whitespace will be trimmed.
115
+ * @param startDate - The start time of the operation
116
+ * @param endDate - The end time of the operation (must be after startDate)
117
+ *
118
+ * @returns Promise<void>
119
+ */
120
+ export const addCompletedCustomSpan = async (name, startDate, endDate) => {
121
+ try {
122
+ // Validate name
123
+ const trimmedName = name.trim();
124
+ if (trimmedName.length === 0) {
125
+ console.error(LuciqStrings.customSpanNameEmpty);
126
+ return;
127
+ }
128
+ // Validate timestamps
129
+ if (endDate <= startDate) {
130
+ console.error(LuciqStrings.customSpanEndTimeBeforeStartTime);
131
+ return;
132
+ }
133
+ // Check SDK initialization
134
+ const isInitialized = await NativeLuciq.isBuilt();
135
+ if (!isInitialized) {
136
+ console.error(LuciqStrings.customSpanSDKNotInitializedMessage);
137
+ return;
138
+ }
139
+ // Check APM enabled
140
+ const isAPMEnabled = await NativeAPM.isAPMEnabled();
141
+ if (!isAPMEnabled) {
142
+ console.log(LuciqStrings.customSpanAPMDisabledMessage);
143
+ return;
144
+ }
145
+ // Check custom spans enabled
146
+ const isCustomSpanEnabled = await NativeAPM.isCustomSpanEnabled();
147
+ if (!isCustomSpanEnabled) {
148
+ console.log(LuciqStrings.customSpanDisabled);
149
+ return;
150
+ }
151
+ // Truncate name if needed
152
+ let spanName = trimmedName;
153
+ if (spanName.length > 150) {
154
+ spanName = spanName.substring(0, 150);
155
+ console.log(LuciqStrings.customSpanNameTruncated);
156
+ }
157
+ // Convert to microseconds
158
+ const startMicros = startDate.getTime() * 1000;
159
+ const endMicros = endDate.getTime() * 1000;
160
+ // Send to native SDK
161
+ await syncCustomSpan(spanName, startMicros, endMicros);
162
+ }
163
+ catch (error) {
164
+ console.error('[CustomSpan] Error adding completed span:', error);
165
+ }
166
+ };
167
+ /**
168
+ * Test-only helper to clear active spans between tests.
169
+ * @internal
170
+ */
171
+ export const __resetCustomSpansForTests = () => {
172
+ activeSpans.clear();
173
+ };
@@ -1,3 +1,9 @@
1
+ export declare function initFeatureFlagsCache(): Promise<void>;
2
+ export declare function getCachedW3cFlags(): {
3
+ isW3cExternalTraceIDEnabled: boolean;
4
+ isW3cExternalGeneratedHeaderEnabled: boolean;
5
+ isW3cCaughtHeaderEnabled: boolean;
6
+ };
1
7
  export declare const FeatureFlags: {
2
8
  isW3ExternalTraceID: () => Promise<boolean>;
3
9
  isW3ExternalGeneratedHeader: () => Promise<boolean>;
@@ -1,5 +1,34 @@
1
1
  import { NativeLuciq } from '../native/NativeLuciq';
2
2
  import { _registerFeatureFlagsChangeListener } from '../modules/Luciq';
3
+ import { Logger } from './logger';
4
+ const TAG = 'LCQ-RN-NET:';
5
+ let cachedW3cFlags = {
6
+ isW3cExternalTraceIDEnabled: false,
7
+ isW3cExternalGeneratedHeaderEnabled: false,
8
+ isW3cCaughtHeaderEnabled: false,
9
+ };
10
+ export async function initFeatureFlagsCache() {
11
+ Logger.debug(TAG, '[FeatureFlags] Initializing W3C feature flags cache from native bridge...');
12
+ try {
13
+ const [traceID, generatedHeader, caughtHeader] = await Promise.all([
14
+ NativeLuciq.isW3ExternalTraceIDEnabled(),
15
+ NativeLuciq.isW3ExternalGeneratedHeaderEnabled(),
16
+ NativeLuciq.isW3CaughtHeaderEnabled(),
17
+ ]);
18
+ cachedW3cFlags = {
19
+ isW3cExternalTraceIDEnabled: traceID,
20
+ isW3cExternalGeneratedHeaderEnabled: generatedHeader,
21
+ isW3cCaughtHeaderEnabled: caughtHeader,
22
+ };
23
+ Logger.debug(TAG, `[FeatureFlags] Cache initialized: traceID=${traceID}, generatedHeader=${generatedHeader}, caughtHeader=${caughtHeader}`);
24
+ }
25
+ catch (e) {
26
+ Logger.debug(TAG, '[FeatureFlags] Failed to initialize cache, using defaults (all false):', e);
27
+ }
28
+ }
29
+ export function getCachedW3cFlags() {
30
+ return cachedW3cFlags;
31
+ }
3
32
  export const FeatureFlags = {
4
33
  isW3ExternalTraceID: () => NativeLuciq.isW3ExternalTraceIDEnabled(),
5
34
  isW3ExternalGeneratedHeader: () => NativeLuciq.isW3ExternalGeneratedHeaderEnabled(),
@@ -8,6 +37,12 @@ export const FeatureFlags = {
8
37
  };
9
38
  export const registerFeatureFlagsListener = () => {
10
39
  _registerFeatureFlagsChangeListener((res) => {
40
+ Logger.debug(TAG, `[FeatureFlags] Flags updated from native listener: traceID=${res.isW3ExternalTraceIDEnabled}, generatedHeader=${res.isW3ExternalGeneratedHeaderEnabled}, caughtHeader=${res.isW3CaughtHeaderEnabled}, bodyLimit=${res.networkBodyLimit}`);
41
+ cachedW3cFlags = {
42
+ isW3cExternalTraceIDEnabled: res.isW3ExternalTraceIDEnabled,
43
+ isW3cExternalGeneratedHeaderEnabled: res.isW3ExternalGeneratedHeaderEnabled,
44
+ isW3cCaughtHeaderEnabled: res.isW3CaughtHeaderEnabled,
45
+ };
11
46
  FeatureFlags.isW3ExternalTraceID = async () => {
12
47
  return res.isW3ExternalTraceIDEnabled;
13
48
  };
@@ -3,6 +3,7 @@ import parseErrorStackLib from 'react-native/Libraries/Core/Devtools/parseErrorS
3
3
  import { NativeCrashReporting } from '../native/NativeCrashReporting';
4
4
  import { NativeLuciq } from '../native/NativeLuciq';
5
5
  import { NativeAPM } from '../native/NativeAPM';
6
+ import { Logger } from './logger';
6
7
  import * as NetworkLogger from '../modules/NetworkLogger';
7
8
  import { NativeNetworkLogger, NativeNetworkLoggerEvent, NetworkListenerType, NetworkLoggerEmitter, } from '../native/NativeNetworkLogger';
8
9
  let apmFlags = {
@@ -181,10 +182,12 @@ export const reportNetworkLog = (network) => {
181
182
  if (Platform.OS === 'android') {
182
183
  const requestHeaders = JSON.stringify(network.requestHeaders);
183
184
  const responseHeaders = JSON.stringify(network.responseHeaders);
185
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Sending to NativeLuciq.networkLogAndroid: ${network.method} ${network.url}, status=${network.responseCode}, duration=${network.duration}ms, error=${network.errorDomain || 'none'}`);
184
186
  NativeLuciq.networkLogAndroid(network.url, network.requestBody, network.responseBody, network.method, network.responseCode, requestHeaders, responseHeaders, network.duration);
185
187
  if (!apmFlags.isNativeInterceptionFeatureEnabled ||
186
188
  !apmFlags.hasAPMNetworkPlugin ||
187
189
  !apmFlags.shouldEnableNativeInterception) {
190
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Also sending to NativeAPM.networkLogAndroid (native interception disabled): ${network.method} ${network.url}`);
188
191
  NativeAPM.networkLogAndroid(network.startTime, network.duration, requestHeaders, network.requestBody, network.requestBodySize, network.method, network.url, network.requestContentType, responseHeaders, network.responseBody, network.responseBodySize, network.responseCode, network.contentType, network.errorDomain, {
189
192
  isW3cHeaderFound: network.isW3cHeaderFound,
190
193
  partialId: network.partialId,
@@ -193,6 +196,9 @@ export const reportNetworkLog = (network) => {
193
196
  w3cCaughtHeader: network.w3cCaughtHeader,
194
197
  }, network.gqlQueryName, network.serverErrorMessage);
195
198
  }
199
+ else {
200
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Skipping NativeAPM.networkLogAndroid (native interception enabled): nativeFeature=${apmFlags.isNativeInterceptionFeatureEnabled}, hasPlugin=${apmFlags.hasAPMNetworkPlugin}, shouldEnable=${apmFlags.shouldEnableNativeInterception}`);
201
+ }
196
202
  }
197
203
  else {
198
204
  NativeLuciq.networkLogIOS(network.url, network.method, network.requestBody, network.requestBodySize, network.responseBody, network.responseBodySize, network.responseCode, network.requestHeaders, network.responseHeaders, network.contentType, network.errorDomain, network.errorCode, network.startTime, network.duration, network.gqlQueryName, network.serverErrorMessage, {
@@ -1,6 +1,8 @@
1
1
  import LuciqConstants from './LuciqConstants';
2
2
  import { stringifyIfNotString, generateW3CHeader } from './LuciqUtils';
3
- import { FeatureFlags } from '../utils/FeatureFlags';
3
+ import { getCachedW3cFlags } from './FeatureFlags';
4
+ import { Logger } from './logger';
5
+ const TAG = 'LCQ-RN-NET:';
4
6
  const XMLHttpRequest = global.XMLHttpRequest;
5
7
  let originalXHROpen = XMLHttpRequest.prototype.open;
6
8
  let originalXHRSend = XMLHttpRequest.prototype.send;
@@ -8,40 +10,34 @@ let originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
8
10
  let onProgressCallback;
9
11
  let onDoneCallback;
10
12
  let isInterceptorEnabled = false;
11
- let network;
12
- const _reset = () => {
13
- network = {
14
- id: '',
15
- url: '',
16
- method: '',
17
- requestBody: '',
18
- requestBodySize: 0,
19
- responseBody: '',
20
- responseBodySize: 0,
21
- responseCode: 0,
22
- requestHeaders: {},
23
- responseHeaders: {},
24
- contentType: '',
25
- errorDomain: '',
26
- errorCode: 0,
27
- startTime: 0,
28
- duration: 0,
29
- gqlQueryName: '',
30
- serverErrorMessage: '',
31
- requestContentType: '',
32
- isW3cHeaderFound: null,
33
- partialId: null,
34
- networkStartTimeInSeconds: null,
35
- w3cGeneratedHeader: null,
36
- w3cCaughtHeader: null,
37
- };
38
- };
39
- const getTraceparentHeader = async (networkData) => {
40
- const [isW3cExternalTraceIDEnabled, isW3cExternalGeneratedHeaderEnabled, isW3cCaughtHeaderEnabled,] = await Promise.all([
41
- FeatureFlags.isW3ExternalTraceID(),
42
- FeatureFlags.isW3ExternalGeneratedHeader(),
43
- FeatureFlags.isW3CaughtHeader(),
44
- ]);
13
+ const networkMap = new WeakMap();
14
+ const createNetworkData = () => ({
15
+ id: '',
16
+ url: '',
17
+ method: '',
18
+ requestBody: '',
19
+ requestBodySize: 0,
20
+ responseBody: '',
21
+ responseBodySize: 0,
22
+ responseCode: 0,
23
+ requestHeaders: {},
24
+ responseHeaders: {},
25
+ contentType: '',
26
+ errorDomain: '',
27
+ errorCode: 0,
28
+ startTime: 0,
29
+ duration: 0,
30
+ gqlQueryName: '',
31
+ serverErrorMessage: '',
32
+ requestContentType: '',
33
+ isW3cHeaderFound: null,
34
+ partialId: null,
35
+ networkStartTimeInSeconds: null,
36
+ w3cGeneratedHeader: null,
37
+ w3cCaughtHeader: null,
38
+ });
39
+ const getTraceparentHeader = (networkData) => {
40
+ const { isW3cExternalTraceIDEnabled, isW3cExternalGeneratedHeaderEnabled, isW3cCaughtHeaderEnabled, } = getCachedW3cFlags();
45
41
  return injectHeaders(networkData, {
46
42
  isW3cExternalTraceIDEnabled,
47
43
  isW3cExternalGeneratedHeaderEnabled,
@@ -85,39 +81,57 @@ export default {
85
81
  onProgressCallback = callback;
86
82
  },
87
83
  enableInterception() {
88
- // Prevents infinite calls to XMLHttpRequest.open when enabling interception multiple times
89
84
  if (isInterceptorEnabled) {
85
+ Logger.debug(TAG, 'enableInterception called but already enabled, skipping');
90
86
  return;
91
87
  }
88
+ Logger.debug(TAG, 'Enabling XHR network interception');
92
89
  originalXHROpen = XMLHttpRequest.prototype.open;
93
90
  originalXHRSend = XMLHttpRequest.prototype.send;
94
91
  originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
95
92
  // An error code that signifies an issue with the RN client.
96
93
  const clientErrorCode = 9876;
97
94
  XMLHttpRequest.prototype.open = function (method, url, ...args) {
98
- _reset();
99
- network.url = url;
100
- network.method = method;
95
+ const networkData = createNetworkData();
96
+ networkData.url = url;
97
+ networkData.method = method;
98
+ networkMap.set(this, networkData);
99
+ Logger.debug(TAG, `[open] ${method} ${url}`);
101
100
  originalXHROpen.apply(this, [method, url, ...args]);
102
101
  };
103
102
  XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
104
- // According to the HTTP RFC, headers are case-insensitive, so we convert
105
- // them to lower-case to make accessing headers predictable.
106
- // This avoid issues like failing to get the Content-Type header for a request
107
- // because the header is set as 'Content-Type' instead of 'content-type'.
108
103
  const key = header.toLowerCase();
109
- network.requestHeaders[key] = stringifyIfNotString(value);
104
+ const networkData = networkMap.get(this);
105
+ if (networkData) {
106
+ networkData.requestHeaders[key] = stringifyIfNotString(value);
107
+ }
108
+ else {
109
+ Logger.debug(TAG, `[setRequestHeader] No networkData found in WeakMap for header "${key}" — request may have been GC'd or open() was not called`);
110
+ }
110
111
  originalXHRSetRequestHeader.apply(this, [header, value]);
111
112
  };
112
- XMLHttpRequest.prototype.send = async function (data) {
113
- const cloneNetwork = JSON.parse(JSON.stringify(network));
113
+ XMLHttpRequest.prototype.send = function (data) {
114
+ const networkData = networkMap.get(this);
115
+ if (!networkData) {
116
+ Logger.debug(TAG, '[send] No networkData found in WeakMap — falling back to original send (open() was not intercepted)');
117
+ originalXHRSend.apply(this, [data]);
118
+ return;
119
+ }
120
+ Logger.debug(TAG, `[send] ${networkData.method} ${networkData.url}`);
121
+ const cloneNetwork = JSON.parse(JSON.stringify(networkData));
114
122
  cloneNetwork.requestBody = data ? data : '';
115
123
  if (typeof cloneNetwork.requestBody !== 'string') {
116
124
  cloneNetwork.requestBody = JSON.stringify(cloneNetwork.requestBody);
117
125
  }
126
+ let isReported = false;
118
127
  if (this.addEventListener) {
119
128
  this.addEventListener('readystatechange', async () => {
120
129
  if (!isInterceptorEnabled) {
130
+ Logger.debug(TAG, `[readystatechange] Interceptor disabled, ignoring state=${this.readyState} for ${cloneNetwork.url}`);
131
+ return;
132
+ }
133
+ if (isReported) {
134
+ Logger.debug(TAG, `[readystatechange] Already reported, ignoring state=${this.readyState} for ${cloneNetwork.url}`);
121
135
  return;
122
136
  }
123
137
  if (this.readyState === this.HEADERS_RECEIVED) {
@@ -146,6 +160,7 @@ export default {
146
160
  cloneNetwork.requestContentType =
147
161
  cloneNetwork.requestHeaders['content-type'].split(';')[0];
148
162
  }
163
+ Logger.debug(TAG, `[readystatechange] HEADERS_RECEIVED for ${cloneNetwork.url}, contentType=${cloneNetwork.contentType}`);
149
164
  }
150
165
  if (this.readyState === this.DONE) {
151
166
  cloneNetwork.duration = Date.now() - cloneNetwork.startTime;
@@ -166,11 +181,11 @@ export default {
166
181
  cloneNetwork.requestBody =
167
182
  typeof _response === 'string' ? _response : JSON.stringify(_response);
168
183
  cloneNetwork.responseBody = '';
169
- // Detect a more descriptive error message.
170
184
  if (typeof _response === 'string' && _response.length > 0) {
171
185
  cloneNetwork.errorDomain = _response;
172
186
  }
173
187
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
188
+ Logger.debug(TAG, `[readystatechange] DONE with client error for ${cloneNetwork.url}, errorDomain=${cloneNetwork.errorDomain}`);
174
189
  // @ts-ignore
175
190
  }
176
191
  else if (this._timedOut) {
@@ -179,6 +194,7 @@ export default {
179
194
  cloneNetwork.responseCode = 0;
180
195
  cloneNetwork.contentType = 'text/plain';
181
196
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
197
+ Logger.debug(TAG, `[readystatechange] DONE with timeout for ${cloneNetwork.url}`);
182
198
  }
183
199
  // Only set response body if not already set by error handlers
184
200
  if (!cloneNetwork.errorDomain) {
@@ -225,16 +241,20 @@ export default {
225
241
  else {
226
242
  delete cloneNetwork.gqlQueryName;
227
243
  }
244
+ isReported = true;
245
+ Logger.debug(TAG, `[readystatechange] DONE for ${cloneNetwork.method} ${cloneNetwork.url} — status=${cloneNetwork.responseCode}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`);
228
246
  if (onDoneCallback) {
229
247
  onDoneCallback(cloneNetwork);
230
248
  }
249
+ else {
250
+ Logger.debug(TAG, `[readystatechange] WARNING: onDoneCallback is null, network log for ${cloneNetwork.url} will be LOST`);
251
+ }
231
252
  }
232
253
  });
233
254
  const downloadUploadProgressCallback = (event) => {
234
255
  if (!isInterceptorEnabled) {
235
256
  return;
236
257
  }
237
- // check if will be able to compute progress
238
258
  if (event.lengthComputable && onProgressCallback) {
239
259
  const totalBytesSent = event.loaded;
240
260
  const totalBytesExpectedToSend = event.total - event.loaded;
@@ -243,31 +263,43 @@ export default {
243
263
  };
244
264
  this.addEventListener('progress', downloadUploadProgressCallback);
245
265
  this.upload.addEventListener('progress', downloadUploadProgressCallback);
246
- // Handler for abort events (works with fetch, Axios, and any XHR-based requests)
247
266
  this.addEventListener('abort', () => {
248
267
  if (!isInterceptorEnabled) {
268
+ Logger.debug(TAG, `[abort] Interceptor disabled, ignoring abort for ${cloneNetwork.url}`);
269
+ return;
270
+ }
271
+ if (isReported) {
272
+ Logger.debug(TAG, `[abort] Already reported via readystatechange DONE, ignoring duplicate abort for ${cloneNetwork.url}`);
249
273
  return;
250
274
  }
275
+ isReported = true;
251
276
  cloneNetwork.duration = Date.now() - cloneNetwork.startTime;
252
277
  cloneNetwork.responseCode = 0;
253
278
  cloneNetwork.errorCode = clientErrorCode;
254
279
  cloneNetwork.errorDomain = 'cancelled';
255
280
  cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
281
+ Logger.debug(TAG, `[abort] Request cancelled: ${cloneNetwork.method} ${cloneNetwork.url}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`);
282
+ if (onDoneCallback) {
283
+ onDoneCallback(cloneNetwork);
284
+ }
285
+ else {
286
+ Logger.debug(TAG, `[abort] WARNING: onDoneCallback is null, cancelled log for ${cloneNetwork.url} will be LOST`);
287
+ }
256
288
  });
257
289
  }
258
290
  cloneNetwork.startTime = Date.now();
259
- const traceparent = await getTraceparentHeader(cloneNetwork);
291
+ const traceparent = getTraceparentHeader(cloneNetwork);
260
292
  if (traceparent) {
261
293
  this.setRequestHeader('Traceparent', traceparent);
262
- }
263
- if (this.readyState === this.UNSENT) {
264
- return; // Prevent sending the request if not opened
294
+ Logger.debug(TAG, `[send] Injected traceparent header for ${cloneNetwork.url}`);
265
295
  }
266
296
  originalXHRSend.apply(this, [data]);
267
297
  };
268
298
  isInterceptorEnabled = true;
299
+ Logger.debug(TAG, 'XHR network interception enabled');
269
300
  },
270
301
  disableInterception() {
302
+ Logger.debug(TAG, 'Disabling XHR network interception');
271
303
  isInterceptorEnabled = false;
272
304
  XMLHttpRequest.prototype.send = originalXHRSend;
273
305
  XMLHttpRequest.prototype.open = originalXHROpen;
@@ -23,4 +23,17 @@
23
23
 
24
24
  - (void)setScreenRenderingEnabled:(BOOL)isEnabled;
25
25
 
26
+ // Custom Span methods
27
+ - (void)syncCustomSpan:(NSString *)name
28
+ startTimestamp:(double)startTimestamp
29
+ endTimestamp:(double)endTimestamp
30
+ resolver:(RCTPromiseResolveBlock)resolve
31
+ rejecter:(RCTPromiseRejectBlock)reject;
32
+
33
+ - (void)isCustomSpanEnabled:(RCTPromiseResolveBlock)resolve
34
+ rejecter:(RCTPromiseRejectBlock)reject;
35
+
36
+ - (void)isAPMEnabled:(RCTPromiseResolveBlock)resolve
37
+ rejecter:(RCTPromiseRejectBlock)reject;
38
+
26
39
  @end
@@ -90,6 +90,61 @@ RCT_EXPORT_METHOD(setScreenRenderingEnabled:(BOOL)isEnabled) {
90
90
  LCQAPM.screenRenderingEnabled = isEnabled;
91
91
  }
92
92
 
93
+ // Syncs a custom span to the native SDK (currently logs only)
94
+ RCT_EXPORT_METHOD(syncCustomSpan:(NSString *)name
95
+ startTimestamp:(double)startTimestamp
96
+ endTimestamp:(double)endTimestamp
97
+ resolver:(RCTPromiseResolveBlock)resolve
98
+ rejecter:(RCTPromiseRejectBlock)reject)
99
+ {
100
+ @try {
101
+ // Convert microseconds → seconds (NSDate uses seconds)
102
+ NSTimeInterval startSeconds = startTimestamp / 1e6;
103
+ NSTimeInterval endSeconds = endTimestamp / 1e6;
104
+
105
+ NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:startSeconds];
106
+ NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:endSeconds];
107
+
108
+ // Add completed span to APM
109
+ [LCQAPM addCompletedCustomSpanWithName:name
110
+ startDate:startDate
111
+ endDate:endDate];
112
+
113
+ resolve(@YES);
114
+ }
115
+ @catch (NSException *exception) {
116
+ reject(
117
+ @"SYNC_CUSTOM_SPAN_ERROR",
118
+ exception.reason ?: @"Failed to sync custom span",
119
+ nil
120
+ );
121
+ }
122
+ }
123
+
124
+ // Checks if custom spans feature is enabled
125
+ RCT_EXPORT_METHOD(isCustomSpanEnabled:(RCTPromiseResolveBlock)resolve
126
+ rejecter:(RCTPromiseRejectBlock)reject) {
127
+ @try {
128
+ BOOL enabled = LCQAPM.customSpansEnabled;
129
+ resolve(@(enabled));
130
+ } @catch (NSException *exception) {
131
+ NSLog(@"[CustomSpan] Error checking feature flag: %@", exception);
132
+ resolve(@NO);
133
+ }
134
+ }
135
+
136
+ // Checks if APM is enabled
137
+ RCT_EXPORT_METHOD(isAPMEnabled:(RCTPromiseResolveBlock)resolve
138
+ rejecter:(RCTPromiseRejectBlock)reject) {
139
+ @try {
140
+ BOOL enabled = LCQAPM.enabled;
141
+ resolve(@(enabled));
142
+ } @catch (NSException *exception) {
143
+ NSLog(@"[CustomSpan] Error checking APM enabled: %@", exception);
144
+ resolve(@NO);
145
+ }
146
+ }
147
+
93
148
 
94
149
 
95
150
  @synthesize description;
@@ -545,4 +545,16 @@ RCT_EXPORT_METHOD(setNetworkLogBodyEnabled:(BOOL)isEnabled) {
545
545
  LCQNetworkLogger.logBodyEnabled = isEnabled;
546
546
  }
547
547
 
548
+ // Checks if Luciq SDK is initialized
549
+ RCT_EXPORT_METHOD(isBuilt:(RCTPromiseResolveBlock)resolve
550
+ rejecter:(RCTPromiseRejectBlock)reject) {
551
+ @try {
552
+ BOOL isBuilt = YES;
553
+ resolve(@(isBuilt));
554
+ } @catch (NSException *exception) {
555
+ NSLog(@"[Luciq] Error checking if SDK is built: %@", exception);
556
+ resolve(@NO);
557
+ }
558
+ }
559
+
548
560
  @end
@@ -11,5 +11,6 @@
11
11
 
12
12
  @property (class, atomic, assign) BOOL networkEnabled;
13
13
 
14
+ + (BOOL)customSpansEnabled;
14
15
 
15
16
  @end
package/ios/native.rb CHANGED
@@ -1,4 +1,4 @@
1
- $luciq= { :version => '19.4.1' }
1
+ $luciq= { :version => '19.5.0' }
2
2
 
3
3
  def use_luciq! (spec = nil)
4
4
  version = $luciq[:version]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@luciq/react-native",
3
3
  "description": "Luciq is the Agentic Observability Platform built for Mobile.",
4
- "version": "19.2.2",
4
+ "version": "19.3.0-40271-SNAPSHOT",
5
5
  "author": "Luciq (https://luciq.ai)",
6
6
  "repository": "github:luciqai/luciq-reactnative-sdk",
7
7
  "homepage": "https://www.luciq.ai/platforms/react-native",
@@ -43,7 +43,6 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@apollo/client": "^3.7.0",
46
- "@instabug/danger-plugin-coverage": "Instabug/danger-plugin-coverage",
47
46
  "@react-native-community/eslint-config": "^3.1.0",
48
47
  "@react-navigation/native": "^6.1.7",
49
48
  "@rollup/plugin-commonjs": "^25.0.3",
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ input=$(cat)
3
+
4
+ input=$(sed -E \
5
+ -e 's/\[([^]]+)\]\(([^)]+)\)/<\2|\1>/g' \
6
+ -e 's/^#{1,6}[[:space:]]*([^[:space:]].*)$/\*\1\*/' \
7
+ -e 's/^- /• /' <<< "$input")
8
+
9
+ echo "$input"