@luciq/react-native 19.3.0 → 19.4.0-40394-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.
- package/CHANGELOG.md +15 -0
- package/android/native.gradle +1 -1
- package/android/proguard-rules.txt +1 -1
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +9 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +74 -9
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativePackage.java +2 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
- package/android/src/main/java/ai/luciq/reactlibrary/utils/ReportUtil.java +0 -7
- package/dist/modules/Luciq.d.ts +15 -0
- package/dist/modules/Luciq.js +23 -1
- package/dist/modules/NetworkLogger.d.ts +0 -5
- package/dist/modules/NetworkLogger.js +9 -1
- package/dist/native/NativeLuciq.d.ts +3 -0
- package/dist/utils/FeatureFlags.d.ts +6 -0
- package/dist/utils/FeatureFlags.js +35 -0
- package/dist/utils/LuciqUtils.js +6 -0
- package/dist/utils/XhrNetworkInterceptor.js +85 -53
- package/ios/RNLuciq/LuciqReactBridge.m +12 -0
- package/ios/native.rb +1 -1
- package/package.json +1 -2
- package/src/modules/Luciq.ts +27 -1
- package/src/modules/NetworkLogger.ts +26 -1
- package/src/native/NativeLuciq.ts +5 -0
- package/src/utils/FeatureFlags.ts +44 -0
- package/src/utils/LuciqUtils.ts +15 -0
- package/src/utils/XhrNetworkInterceptor.ts +128 -55
|
@@ -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
|
};
|
package/dist/utils/LuciqUtils.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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 =
|
|
113
|
-
const
|
|
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 =
|
|
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;
|
|
@@ -98,6 +98,18 @@ RCT_EXPORT_METHOD(setTrackUserSteps:(BOOL)isEnabled) {
|
|
|
98
98
|
[Luciq setTrackUserSteps:isEnabled];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
RCT_EXPORT_METHOD(setWebViewMonitoringEnabled:(BOOL)isEnabled) {
|
|
102
|
+
[Luciq setWebViewMonitoringEnabled:isEnabled];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RCT_EXPORT_METHOD(setWebViewNetworkTrackingEnabled:(BOOL)isEnabled) {
|
|
106
|
+
[Luciq setWebViewNetworkTrackingEnabled:isEnabled];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
RCT_EXPORT_METHOD(setWebViewUserInteractionsTrackingEnabled:(BOOL)isEnabled) {
|
|
110
|
+
[Luciq setWebViewUserInteractionsTrackingEnabled:isEnabled];
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
LCQReport *currentReport = nil;
|
|
102
114
|
RCT_EXPORT_METHOD(setPreSendingHandler:(RCTResponseSenderBlock)callBack) {
|
|
103
115
|
if (callBack != nil) {
|
package/ios/native.rb
CHANGED
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.
|
|
4
|
+
"version": "19.4.0-40394-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",
|
package/src/modules/Luciq.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { NavigationAction, NavigationState as NavigationStateV4 } from 'rea
|
|
|
10
10
|
import type { LuciqConfig } from '../models/LuciqConfig';
|
|
11
11
|
import Report from '../models/Report';
|
|
12
12
|
import { emitter, NativeEvents, NativeLuciq } from '../native/NativeLuciq';
|
|
13
|
-
import { registerFeatureFlagsListener } from '../utils/FeatureFlags';
|
|
13
|
+
import { registerFeatureFlagsListener, initFeatureFlagsCache } from '../utils/FeatureFlags';
|
|
14
14
|
import {
|
|
15
15
|
AutoMaskingType,
|
|
16
16
|
ColorTheme,
|
|
@@ -81,6 +81,8 @@ function reportCurrentViewForAndroid(screenName: string | null) {
|
|
|
81
81
|
* @param config SDK configurations. See {@link LuciqConfig} for more info.
|
|
82
82
|
*/
|
|
83
83
|
export const init = (config: LuciqConfig) => {
|
|
84
|
+
initFeatureFlagsCache();
|
|
85
|
+
|
|
84
86
|
if (Platform.OS === 'android') {
|
|
85
87
|
// Add android feature flags listener for android
|
|
86
88
|
registerFeatureFlagsListener();
|
|
@@ -130,6 +132,30 @@ export const setAppVariant = (appVariant: string) => {
|
|
|
130
132
|
NativeLuciq.setAppVariant(appVariant);
|
|
131
133
|
};
|
|
132
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Enables or disables WebView monitoring.
|
|
137
|
+
* @param isEnabled A boolean to enable/disable WebView monitoring.
|
|
138
|
+
*/
|
|
139
|
+
export const setWebViewMonitoringEnabled = (isEnabled: boolean) => {
|
|
140
|
+
NativeLuciq.setWebViewMonitoringEnabled(isEnabled);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Enables or disables WebView network tracking.
|
|
145
|
+
* @param isEnabled A boolean to enable/disable WebView network tracking.
|
|
146
|
+
*/
|
|
147
|
+
export const setWebViewNetworkTrackingEnabled = (isEnabled: boolean) => {
|
|
148
|
+
NativeLuciq.setWebViewNetworkTrackingEnabled(isEnabled);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Enables or disables WebView user interactions tracking.
|
|
153
|
+
* @param isEnabled A boolean to enable/disable WebView user interactions tracking.
|
|
154
|
+
*/
|
|
155
|
+
export const setWebViewUserInteractionsTrackingEnabled = (isEnabled: boolean) => {
|
|
156
|
+
NativeLuciq.setWebViewUserInteractionsTrackingEnabled(isEnabled);
|
|
157
|
+
};
|
|
158
|
+
|
|
133
159
|
/**
|
|
134
160
|
* Handles app state changes and updates APM network flags if necessary.
|
|
135
161
|
*/
|
|
@@ -39,10 +39,17 @@ function getPortFromUrl(url: string) {
|
|
|
39
39
|
* It is enabled by default.
|
|
40
40
|
* @param isEnabled
|
|
41
41
|
*/
|
|
42
|
+
const NET_TAG = 'LCQ-RN-NET:';
|
|
43
|
+
|
|
42
44
|
export const setEnabled = (isEnabled: boolean) => {
|
|
43
45
|
if (isEnabled) {
|
|
44
46
|
xhr.enableInterception();
|
|
45
47
|
xhr.setOnDoneCallback(async (network) => {
|
|
48
|
+
Logger.debug(
|
|
49
|
+
NET_TAG,
|
|
50
|
+
`[NetworkLogger] onDoneCallback received: ${network.method} ${network.url}, status=${network.responseCode}`,
|
|
51
|
+
);
|
|
52
|
+
|
|
46
53
|
// eslint-disable-next-line no-new-func
|
|
47
54
|
const predicate = Function('network', 'return ' + _requestFilterExpression);
|
|
48
55
|
|
|
@@ -50,12 +57,17 @@ export const setEnabled = (isEnabled: boolean) => {
|
|
|
50
57
|
const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeLuciq.getNetworkBodyMaxSize();
|
|
51
58
|
try {
|
|
52
59
|
if (_networkDataObfuscationHandler) {
|
|
60
|
+
Logger.debug(NET_TAG, `[NetworkLogger] Running obfuscation handler for ${network.url}`);
|
|
53
61
|
network = await _networkDataObfuscationHandler(network);
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
if (__DEV__) {
|
|
57
65
|
const urlPort = getPortFromUrl(network.url);
|
|
58
66
|
if (urlPort === LuciqRNConfig.metroDevServerPort) {
|
|
67
|
+
Logger.debug(
|
|
68
|
+
NET_TAG,
|
|
69
|
+
`[NetworkLogger] Skipping Metro dev server request: ${network.url}`,
|
|
70
|
+
);
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
}
|
|
@@ -97,10 +109,23 @@ export const setEnabled = (isEnabled: boolean) => {
|
|
|
97
109
|
);
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
Logger.debug(
|
|
113
|
+
NET_TAG,
|
|
114
|
+
`[NetworkLogger] Reporting network log to native: ${network.method} ${network.url}`,
|
|
115
|
+
);
|
|
100
116
|
reportNetworkLog(network);
|
|
101
117
|
} catch (e) {
|
|
102
|
-
Logger.error(
|
|
118
|
+
Logger.error(
|
|
119
|
+
NET_TAG,
|
|
120
|
+
`[NetworkLogger] Error processing network log for ${network.url}:`,
|
|
121
|
+
e,
|
|
122
|
+
);
|
|
103
123
|
}
|
|
124
|
+
} else {
|
|
125
|
+
Logger.debug(
|
|
126
|
+
NET_TAG,
|
|
127
|
+
`[NetworkLogger] Request filtered out by predicate: ${network.method} ${network.url}, expression="${_requestFilterExpression}"`,
|
|
128
|
+
);
|
|
104
129
|
}
|
|
105
130
|
});
|
|
106
131
|
} else {
|
|
@@ -167,6 +167,11 @@ export interface LuciqNativeModule extends NativeModule {
|
|
|
167
167
|
|
|
168
168
|
setTheme(theme: ThemeConfig): void;
|
|
169
169
|
setFullscreen(isEnabled: boolean): void;
|
|
170
|
+
|
|
171
|
+
// WebView APIs //
|
|
172
|
+
setWebViewMonitoringEnabled(isEnabled: boolean): void;
|
|
173
|
+
setWebViewNetworkTrackingEnabled(isEnabled: boolean): void;
|
|
174
|
+
setWebViewUserInteractionsTrackingEnabled(isEnabled: boolean): void;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
export const NativeLuciq = NativeModules.Luciq as LuciqNativeModule;
|
|
@@ -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
|
};
|
package/src/utils/LuciqUtils.ts
CHANGED
|
@@ -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(
|