@ansight/react-native 1.3.0-preview.9 → 1.4.0-preview.3
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 +109 -39
- package/android/build.gradle +2 -2
- package/android/src/main/kotlin/ai/ansight/reactnative/AnsightReactNativeModule.kt +91 -24
- package/index.d.ts +97 -23
- package/index.js +231 -80
- package/ios/AnsightReactNative.swift +97 -31
- package/ios/AnsightReactNativeModule.m +4 -0
- package/network.js +764 -0
- package/package.json +8 -2
- package/react-inspection-hook.js +76 -0
- package/react-tree-geometry.js +61 -0
- package/react-tree-semantics.js +33 -0
- package/session-properties.js +151 -0
package/network.js
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const REDACTED_VALUE = "<redacted>";
|
|
4
|
+
const SCHEMA = "ansight.network-request.v1";
|
|
5
|
+
const MAXIMUM_HEADER_COUNT = 128;
|
|
6
|
+
const MAXIMUM_HEADER_VALUE_LENGTH = 4096;
|
|
7
|
+
const MAXIMUM_ERROR_MESSAGE_LENGTH = 4096;
|
|
8
|
+
const MAXIMUM_URL_LENGTH = 16384;
|
|
9
|
+
const DEFAULT_MAXIMUM_BODY_BYTES = 64 * 1024;
|
|
10
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
11
|
+
"authorization",
|
|
12
|
+
"cookie",
|
|
13
|
+
"proxy-authorization",
|
|
14
|
+
"set-cookie",
|
|
15
|
+
"x-api-key",
|
|
16
|
+
"x-auth-token",
|
|
17
|
+
]);
|
|
18
|
+
const SENSITIVE_QUERY_NAMES = new Set([
|
|
19
|
+
"access_token",
|
|
20
|
+
"accesskey",
|
|
21
|
+
"access_key",
|
|
22
|
+
"api_key",
|
|
23
|
+
"apikey",
|
|
24
|
+
"auth",
|
|
25
|
+
"authorization",
|
|
26
|
+
"client_secret",
|
|
27
|
+
"code",
|
|
28
|
+
"credential",
|
|
29
|
+
"credentials",
|
|
30
|
+
"id_token",
|
|
31
|
+
"jwt",
|
|
32
|
+
"key",
|
|
33
|
+
"password",
|
|
34
|
+
"passwd",
|
|
35
|
+
"refresh_token",
|
|
36
|
+
"sas",
|
|
37
|
+
"sastoken",
|
|
38
|
+
"secret",
|
|
39
|
+
"secret_key",
|
|
40
|
+
"security_token",
|
|
41
|
+
"session_token",
|
|
42
|
+
"sig",
|
|
43
|
+
"signature",
|
|
44
|
+
"token",
|
|
45
|
+
]);
|
|
46
|
+
const AZURE_SAS_FINGERPRINT_NAMES = new Set(["se", "skoid", "sp", "sr", "srt", "ss", "sv"]);
|
|
47
|
+
const AZURE_SAS_QUERY_NAMES = new Set([
|
|
48
|
+
"epk", "erk", "rscc", "rscd", "rsce", "rscl", "rsct", "saoid", "scid", "se",
|
|
49
|
+
"sig", "si", "sip", "ske", "skoid", "sks", "skt", "sktid", "skv", "snapshot",
|
|
50
|
+
"sp", "spk", "spr", "sr", "srk", "srt", "ss", "st", "suoid", "tn", "versionid", "sv",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function truncate(value, maximumLength) {
|
|
54
|
+
const text = String(value);
|
|
55
|
+
return text.length <= maximumLength
|
|
56
|
+
? text
|
|
57
|
+
: `${text.slice(0, maximumLength)}…`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRequired(value, fallback, maximumLength) {
|
|
61
|
+
const normalized = value == null ? "" : String(value).trim();
|
|
62
|
+
return truncate(normalized || fallback, maximumLength);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeOptional(value, maximumLength) {
|
|
66
|
+
if (value == null) return undefined;
|
|
67
|
+
const normalized = String(value).trim();
|
|
68
|
+
return normalized ? truncate(normalized, maximumLength) : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function uniqueLowercase(values) {
|
|
72
|
+
return new Set(
|
|
73
|
+
Array.from(values || [], (value) => String(value).toLowerCase()),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSensitiveHeader(name, options) {
|
|
78
|
+
const lowered = name.toLowerCase();
|
|
79
|
+
if (
|
|
80
|
+
SENSITIVE_HEADER_NAMES.has(lowered) ||
|
|
81
|
+
uniqueLowercase(options.additionalSensitiveHeaderNames).has(lowered)
|
|
82
|
+
) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const compact = lowered.replace(/-/g, "");
|
|
86
|
+
return (
|
|
87
|
+
compact.includes("token") ||
|
|
88
|
+
compact.includes("secret") ||
|
|
89
|
+
compact.includes("apikey")
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function headerEntries(headers) {
|
|
94
|
+
if (!headers) return [];
|
|
95
|
+
if (Array.isArray(headers)) {
|
|
96
|
+
return headers.flatMap((header) => {
|
|
97
|
+
if (Array.isArray(header)) return [[header[0], header[1]]];
|
|
98
|
+
if (header && typeof header === "object") {
|
|
99
|
+
return [[header.name, header.value]];
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (typeof headers.forEach === "function") {
|
|
105
|
+
const entries = [];
|
|
106
|
+
headers.forEach((value, name) => entries.push([name, value]));
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
if (typeof headers === "object") return Object.entries(headers);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sanitizeHeaders(headers, options) {
|
|
114
|
+
return headerEntries(headers)
|
|
115
|
+
.filter(([name]) => name != null && String(name).trim())
|
|
116
|
+
.slice(0, MAXIMUM_HEADER_COUNT)
|
|
117
|
+
.map(([rawName, rawValue]) => {
|
|
118
|
+
const name = normalizeRequired(rawName, "Header", 256);
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
value: isSensitiveHeader(name, options)
|
|
122
|
+
? REDACTED_VALUE
|
|
123
|
+
: normalizeRequired(rawValue, "", MAXIMUM_HEADER_VALUE_LENGTH),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isSensitiveQueryName(name, options, schemes = {}) {
|
|
129
|
+
const lowered = String(name).toLowerCase();
|
|
130
|
+
return (
|
|
131
|
+
SENSITIVE_QUERY_NAMES.has(lowered) ||
|
|
132
|
+
uniqueLowercase(options.additionalSensitiveQueryParameterNames).has(lowered) ||
|
|
133
|
+
(schemes.azure && AZURE_SAS_QUERY_NAMES.has(lowered)) ||
|
|
134
|
+
(schemes.aws && lowered.startsWith("x-amz-")) ||
|
|
135
|
+
(schemes.google && lowered.startsWith("x-goog-")) ||
|
|
136
|
+
(schemes.cloudFront && ["signature", "key-pair-id", "policy", "expires", "hash-algorithm"].includes(lowered)) ||
|
|
137
|
+
(schemes.legacyGoogle && ["signature", "googleaccessid", "expires"].includes(lowered)) ||
|
|
138
|
+
(schemes.alibaba && (lowered.startsWith("x-oss-") || ["signature", "ossaccesskeyid", "security-token"].includes(lowered)))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sanitizeQuery(query, options) {
|
|
143
|
+
if (!query) return "";
|
|
144
|
+
const pairs = query.split("&");
|
|
145
|
+
const decodedNames = new Set(pairs.map((pair) => decodeQueryName(pair).toLowerCase()));
|
|
146
|
+
const schemes = {
|
|
147
|
+
azure: decodedNames.has("sig") && Array.from(AZURE_SAS_FINGERPRINT_NAMES).some((name) => decodedNames.has(name)),
|
|
148
|
+
aws: decodedNames.has("x-amz-signature"),
|
|
149
|
+
google: decodedNames.has("x-goog-signature"),
|
|
150
|
+
cloudFront: decodedNames.has("signature") && ["key-pair-id", "policy", "expires"].some((name) => decodedNames.has(name)),
|
|
151
|
+
legacyGoogle: decodedNames.has("signature") && decodedNames.has("googleaccessid"),
|
|
152
|
+
alibaba: (decodedNames.has("signature") && decodedNames.has("ossaccesskeyid")) || decodedNames.has("x-oss-signature"),
|
|
153
|
+
};
|
|
154
|
+
return pairs
|
|
155
|
+
.map((pair) => {
|
|
156
|
+
const equalsIndex = pair.indexOf("=");
|
|
157
|
+
const encodedName = equalsIndex < 0 ? pair : pair.slice(0, equalsIndex);
|
|
158
|
+
return isSensitiveQueryName(decodeQueryName(pair), options, schemes)
|
|
159
|
+
? `${encodedName}=${encodeURIComponent(REDACTED_VALUE)}`
|
|
160
|
+
: pair;
|
|
161
|
+
})
|
|
162
|
+
.join("&");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decodeQueryName(pair) {
|
|
166
|
+
const equalsIndex = pair.indexOf("=");
|
|
167
|
+
const encodedName = equalsIndex < 0 ? pair : pair.slice(0, equalsIndex);
|
|
168
|
+
try {
|
|
169
|
+
return decodeURIComponent(encodedName.replace(/\+/g, " "));
|
|
170
|
+
} catch (_) {
|
|
171
|
+
return encodedName;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sanitizeUrl(value, options = {}) {
|
|
176
|
+
const normalized = normalizeRequired(value, "<unknown>", MAXIMUM_URL_LENGTH);
|
|
177
|
+
let withoutUserInfo = normalized.replace(
|
|
178
|
+
/^(https?:\/\/)[^/@]+@/i,
|
|
179
|
+
`$1${REDACTED_VALUE}@`,
|
|
180
|
+
);
|
|
181
|
+
const queryIndex = withoutUserInfo.indexOf("?");
|
|
182
|
+
if (queryIndex < 0) return truncate(withoutUserInfo, MAXIMUM_URL_LENGTH);
|
|
183
|
+
const fragmentIndex = withoutUserInfo.indexOf("#", queryIndex);
|
|
184
|
+
if (options.includeQueryString === false) {
|
|
185
|
+
withoutUserInfo =
|
|
186
|
+
withoutUserInfo.slice(0, queryIndex) +
|
|
187
|
+
(fragmentIndex < 0 ? "" : withoutUserInfo.slice(fragmentIndex));
|
|
188
|
+
return truncate(withoutUserInfo, MAXIMUM_URL_LENGTH);
|
|
189
|
+
}
|
|
190
|
+
const queryEnd = fragmentIndex < 0 ? withoutUserInfo.length : fragmentIndex;
|
|
191
|
+
const sanitized =
|
|
192
|
+
withoutUserInfo.slice(0, queryIndex + 1) +
|
|
193
|
+
sanitizeQuery(withoutUserInfo.slice(queryIndex + 1, queryEnd), options) +
|
|
194
|
+
(fragmentIndex < 0 ? "" : withoutUserInfo.slice(fragmentIndex));
|
|
195
|
+
return truncate(sanitized, MAXIMUM_URL_LENGTH);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sanitizeErrorMessage(value, options) {
|
|
199
|
+
const normalized = normalizeOptional(value, MAXIMUM_ERROR_MESSAGE_LENGTH);
|
|
200
|
+
if (!normalized) return undefined;
|
|
201
|
+
return truncate(
|
|
202
|
+
normalized
|
|
203
|
+
.replace(
|
|
204
|
+
/(access_token|api_key|apikey|auth|authorization|code|key|password|passwd|secret|signature|token)(\s*=\s*)([^&\s,;]+)/gi,
|
|
205
|
+
`$1$2${REDACTED_VALUE}`,
|
|
206
|
+
)
|
|
207
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (url) =>
|
|
208
|
+
sanitizeUrl(url, options),
|
|
209
|
+
),
|
|
210
|
+
MAXIMUM_ERROR_MESSAGE_LENGTH,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeTimestamp(value, fallback) {
|
|
215
|
+
const date = new Date(value || fallback);
|
|
216
|
+
return Number.isFinite(date.valueOf()) ? date.toISOString() : fallback;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function generateId(globalObject) {
|
|
220
|
+
if (globalObject.crypto && typeof globalObject.crypto.randomUUID === "function") {
|
|
221
|
+
return globalObject.crypto.randomUUID().replace(/-/g, "");
|
|
222
|
+
}
|
|
223
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeSize(value) {
|
|
227
|
+
const number = Number(value);
|
|
228
|
+
return Number.isFinite(number) && number >= 0 ? Math.round(number) : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function maximumBodyBytes(options) {
|
|
232
|
+
const configured = Number(options.maximumBodyBytes);
|
|
233
|
+
const value = Number.isFinite(configured) ? Math.round(configured) : DEFAULT_MAXIMUM_BODY_BYTES;
|
|
234
|
+
return Math.max(0, value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function encodeUtf8(value, globalObject) {
|
|
238
|
+
if (typeof globalObject.TextEncoder === "function") return new globalObject.TextEncoder().encode(value);
|
|
239
|
+
const encoded = unescape(encodeURIComponent(value));
|
|
240
|
+
return Uint8Array.from(encoded, (character) => character.charCodeAt(0));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function decodeUtf8(bytes, globalObject) {
|
|
244
|
+
if (typeof globalObject.TextDecoder === "function") {
|
|
245
|
+
return new globalObject.TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
246
|
+
}
|
|
247
|
+
let binary = "";
|
|
248
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
249
|
+
try { return decodeURIComponent(escape(binary)); } catch (_) { return ""; }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function bytesToBase64(bytes, globalObject) {
|
|
253
|
+
if (typeof globalObject.btoa !== "function") return undefined;
|
|
254
|
+
let binary = "";
|
|
255
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
256
|
+
return globalObject.btoa(binary);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function base64ToBytes(value, globalObject) {
|
|
260
|
+
if (typeof globalObject.atob !== "function") return undefined;
|
|
261
|
+
const binary = globalObject.atob(value);
|
|
262
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function sanitizeSensitiveText(value, options) {
|
|
266
|
+
return String(value)
|
|
267
|
+
.replace(
|
|
268
|
+
/(access_token|accesskey|access_key|api_key|apikey|auth|authorization|client_secret|code|credential|credentials|id_token|jwt|key|password|passwd|refresh_token|sas|sastoken|secret|secret_key|security_token|session_token|sig|signature|token)(["']?\s*[:=]\s*["']?)([^&\s,;}"']+)/gi,
|
|
269
|
+
`$1$2${REDACTED_VALUE}`,
|
|
270
|
+
)
|
|
271
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (url) => sanitizeUrl(url, options));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeBody(body, options, globalObject) {
|
|
275
|
+
if (!body || maximumBodyBytes(options) <= 0) return undefined;
|
|
276
|
+
const maximum = maximumBodyBytes(options);
|
|
277
|
+
const encoding = String(body.encoding || "").toLowerCase();
|
|
278
|
+
let bytes;
|
|
279
|
+
if (encoding === "utf8") {
|
|
280
|
+
bytes = encodeUtf8(sanitizeSensitiveText(body.data || "", options), globalObject);
|
|
281
|
+
} else if (encoding === "base64" && options.captureBinaryBodies === true) {
|
|
282
|
+
try { bytes = base64ToBytes(String(body.data || ""), globalObject); } catch (_) { return undefined; }
|
|
283
|
+
} else {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
if (!bytes) return undefined;
|
|
287
|
+
const originalLength = bytes.length;
|
|
288
|
+
const captured = bytes.slice(0, maximum);
|
|
289
|
+
const data = encoding === "base64"
|
|
290
|
+
? bytesToBase64(captured, globalObject)
|
|
291
|
+
: decodeUtf8(captured, globalObject);
|
|
292
|
+
if (data == null) return undefined;
|
|
293
|
+
const totalBytes = normalizeSize(body.totalBytes);
|
|
294
|
+
return {
|
|
295
|
+
contentType: normalizeOptional(body.contentType, 512),
|
|
296
|
+
encoding,
|
|
297
|
+
data,
|
|
298
|
+
capturedBytes: encoding === "utf8"
|
|
299
|
+
? encodeUtf8(data, globalObject).length
|
|
300
|
+
: captured.length,
|
|
301
|
+
totalBytes,
|
|
302
|
+
truncated: body.truncated === true || originalLength > captured.length || (totalBytes != null && totalBytes > captured.length),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeRecord(input, options, globalObject) {
|
|
307
|
+
const now = new Date().toISOString();
|
|
308
|
+
const startedAtUtc = normalizeTimestamp(input.startedAtUtc, now);
|
|
309
|
+
const completedAtUtc = normalizeTimestamp(input.completedAtUtc, startedAtUtc);
|
|
310
|
+
const duration = Number(input.durationMilliseconds);
|
|
311
|
+
return {
|
|
312
|
+
schema: SCHEMA,
|
|
313
|
+
id: normalizeRequired(input.id, generateId(globalObject), 128),
|
|
314
|
+
source: normalizeRequired(input.source, "unknown", 128),
|
|
315
|
+
startedAtUtc,
|
|
316
|
+
completedAtUtc:
|
|
317
|
+
completedAtUtc < startedAtUtc ? startedAtUtc : completedAtUtc,
|
|
318
|
+
durationMilliseconds:
|
|
319
|
+
Number.isFinite(duration) && duration >= 0 ? duration : 0,
|
|
320
|
+
method: normalizeRequired(input.method, "GET", 32).toUpperCase(),
|
|
321
|
+
url: sanitizeUrl(input.url, options),
|
|
322
|
+
protocol: normalizeOptional(input.protocol, 64),
|
|
323
|
+
requestHeaders:
|
|
324
|
+
options.includeRequestHeaders === false
|
|
325
|
+
? []
|
|
326
|
+
: sanitizeHeaders(input.requestHeaders, options),
|
|
327
|
+
requestBodySizeBytes:
|
|
328
|
+
options.includeBodySizes === false
|
|
329
|
+
? undefined
|
|
330
|
+
: normalizeSize(input.requestBodySizeBytes),
|
|
331
|
+
requestBody: options.captureRequestBody !== false
|
|
332
|
+
? normalizeBody(input.requestBody, options, globalObject)
|
|
333
|
+
: undefined,
|
|
334
|
+
statusCode:
|
|
335
|
+
Number.isInteger(Number(input.statusCode)) &&
|
|
336
|
+
Number(input.statusCode) >= 100 &&
|
|
337
|
+
Number(input.statusCode) <= 999
|
|
338
|
+
? Number(input.statusCode)
|
|
339
|
+
: undefined,
|
|
340
|
+
reasonPhrase: normalizeOptional(input.reasonPhrase, 512),
|
|
341
|
+
responseHeaders:
|
|
342
|
+
options.includeResponseHeaders === false
|
|
343
|
+
? []
|
|
344
|
+
: sanitizeHeaders(input.responseHeaders, options),
|
|
345
|
+
responseBodySizeBytes:
|
|
346
|
+
options.includeBodySizes === false
|
|
347
|
+
? undefined
|
|
348
|
+
: normalizeSize(input.responseBodySizeBytes),
|
|
349
|
+
responseBody: options.captureResponseBody !== false
|
|
350
|
+
? normalizeBody(input.responseBody, options, globalObject)
|
|
351
|
+
: undefined,
|
|
352
|
+
errorType: normalizeOptional(input.errorType, 512),
|
|
353
|
+
errorMessage: sanitizeErrorMessage(input.errorMessage, options),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function sanitizeNetworkRequest(input, options = {}, globalObject = global) {
|
|
358
|
+
try {
|
|
359
|
+
let normalized = normalizeRecord(input || {}, options, globalObject);
|
|
360
|
+
if (typeof options.urlSanitizer === "function") {
|
|
361
|
+
normalized = normalizeRecord(
|
|
362
|
+
{ ...normalized, url: options.urlSanitizer(normalized.url) },
|
|
363
|
+
options,
|
|
364
|
+
globalObject,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (typeof options.requestSanitizer === "function") {
|
|
368
|
+
const transformed = options.requestSanitizer(normalized);
|
|
369
|
+
if (transformed == null) return null;
|
|
370
|
+
normalized = normalizeRecord(transformed, options, globalObject);
|
|
371
|
+
}
|
|
372
|
+
return normalized;
|
|
373
|
+
} catch (_) {
|
|
374
|
+
// App sanitizers must never affect the HTTP request. Fail closed.
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseContentLength(headers) {
|
|
380
|
+
const entry = headerEntries(headers).find(
|
|
381
|
+
([name]) => String(name).toLowerCase() === "content-length",
|
|
382
|
+
);
|
|
383
|
+
return entry ? normalizeSize(entry[1]) : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function headerValue(headers, wantedName) {
|
|
387
|
+
const entry = headerEntries(headers).find(
|
|
388
|
+
([name]) => String(name).toLowerCase() === wantedName,
|
|
389
|
+
);
|
|
390
|
+
return entry == null ? undefined : String(entry[1]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isTextContentType(contentType) {
|
|
394
|
+
if (!contentType) return true;
|
|
395
|
+
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
396
|
+
return mediaType.startsWith("text/") || mediaType.endsWith("+json") || mediaType.endsWith("+xml") ||
|
|
397
|
+
["application/json", "application/xml", "application/graphql", "application/javascript", "application/x-www-form-urlencoded"].includes(mediaType);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function bodyFromBytes(bytes, totalBytes, contentType, options, globalObject) {
|
|
401
|
+
const binary = !isTextContentType(contentType);
|
|
402
|
+
if (binary && options.captureBinaryBodies !== true) return undefined;
|
|
403
|
+
const maximum = maximumBodyBytes(options);
|
|
404
|
+
if (maximum <= 0) return undefined;
|
|
405
|
+
const captured = bytes.slice(0, maximum);
|
|
406
|
+
const data = binary ? bytesToBase64(captured, globalObject) : decodeUtf8(captured, globalObject);
|
|
407
|
+
if (data == null) return undefined;
|
|
408
|
+
return {
|
|
409
|
+
contentType,
|
|
410
|
+
encoding: binary ? "base64" : "utf8",
|
|
411
|
+
data,
|
|
412
|
+
capturedBytes: binary ? captured.length : encodeUtf8(data, globalObject).length,
|
|
413
|
+
totalBytes,
|
|
414
|
+
truncated: bytes.length > captured.length || (totalBytes != null && totalBytes > captured.length),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function bodyFromValue(value, headers, options, globalObject) {
|
|
419
|
+
if (value == null || options.captureRequestBody === false) return undefined;
|
|
420
|
+
const contentType = headerValue(headers, "content-type");
|
|
421
|
+
if (typeof value === "string") {
|
|
422
|
+
const bytes = encodeUtf8(value, globalObject);
|
|
423
|
+
return bodyFromBytes(bytes, bytes.length, contentType, options, globalObject);
|
|
424
|
+
}
|
|
425
|
+
if (typeof globalObject.URLSearchParams === "function" && value instanceof globalObject.URLSearchParams) {
|
|
426
|
+
const bytes = encodeUtf8(value.toString(), globalObject);
|
|
427
|
+
return bodyFromBytes(bytes, bytes.length, contentType || "application/x-www-form-urlencoded", options, globalObject);
|
|
428
|
+
}
|
|
429
|
+
if (typeof globalObject.ArrayBuffer === "function") {
|
|
430
|
+
if (value instanceof globalObject.ArrayBuffer) {
|
|
431
|
+
const bytes = new Uint8Array(value);
|
|
432
|
+
return bodyFromBytes(bytes, bytes.length, contentType, options, globalObject);
|
|
433
|
+
}
|
|
434
|
+
if (globalObject.ArrayBuffer.isView && globalObject.ArrayBuffer.isView(value)) {
|
|
435
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
436
|
+
return bodyFromBytes(bytes, bytes.length, contentType, options, globalObject);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function bodyFromFetchResponse(
|
|
443
|
+
response,
|
|
444
|
+
headers,
|
|
445
|
+
options,
|
|
446
|
+
globalObject,
|
|
447
|
+
shouldContinue = () => true,
|
|
448
|
+
) {
|
|
449
|
+
if (!shouldContinue() || options.captureResponseBody === false || !response || typeof response.clone !== "function") return undefined;
|
|
450
|
+
const contentType = headerValue(headers, "content-type");
|
|
451
|
+
if (!isTextContentType(contentType) && options.captureBinaryBodies !== true) return undefined;
|
|
452
|
+
const totalBytes = parseContentLength(headers);
|
|
453
|
+
const maximum = maximumBodyBytes(options);
|
|
454
|
+
if (maximum <= 0) return undefined;
|
|
455
|
+
const clone = response.clone();
|
|
456
|
+
if (clone.body && typeof clone.body.getReader === "function") {
|
|
457
|
+
const reader = clone.body.getReader();
|
|
458
|
+
const chunks = [];
|
|
459
|
+
let capturedLength = 0;
|
|
460
|
+
let observedLength = 0;
|
|
461
|
+
try {
|
|
462
|
+
while (capturedLength <= maximum) {
|
|
463
|
+
if (!shouldContinue()) {
|
|
464
|
+
await reader.cancel().catch(() => undefined);
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const result = await reader.read();
|
|
468
|
+
if (!shouldContinue()) {
|
|
469
|
+
await reader.cancel().catch(() => undefined);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
if (result.done) break;
|
|
473
|
+
const chunk = result.value instanceof Uint8Array ? result.value : new Uint8Array(result.value);
|
|
474
|
+
observedLength += chunk.length;
|
|
475
|
+
const remaining = maximum - capturedLength;
|
|
476
|
+
if (remaining > 0) {
|
|
477
|
+
const kept = chunk.slice(0, remaining);
|
|
478
|
+
chunks.push(kept);
|
|
479
|
+
capturedLength += kept.length;
|
|
480
|
+
}
|
|
481
|
+
if (observedLength > maximum) {
|
|
482
|
+
await reader.cancel().catch(() => undefined);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} finally {
|
|
487
|
+
try { reader.releaseLock(); } catch (_) { /* no-op */ }
|
|
488
|
+
}
|
|
489
|
+
const joined = new Uint8Array(capturedLength);
|
|
490
|
+
let offset = 0;
|
|
491
|
+
for (const chunk of chunks) { joined.set(chunk, offset); offset += chunk.length; }
|
|
492
|
+
return bodyFromBytes(joined, totalBytes == null ? observedLength : totalBytes, contentType, options, globalObject);
|
|
493
|
+
}
|
|
494
|
+
if (totalBytes == null || totalBytes > maximum || typeof clone.arrayBuffer !== "function") return undefined;
|
|
495
|
+
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
496
|
+
if (!shouldContinue()) return undefined;
|
|
497
|
+
return bodyFromBytes(bytes, totalBytes, contentType, options, globalObject);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parseXhrResponseHeaders(value) {
|
|
501
|
+
if (!value) return [];
|
|
502
|
+
return String(value)
|
|
503
|
+
.trim()
|
|
504
|
+
.split(/[\r\n]+/)
|
|
505
|
+
.flatMap((line) => {
|
|
506
|
+
const separator = line.indexOf(":");
|
|
507
|
+
return separator < 0
|
|
508
|
+
? []
|
|
509
|
+
: [{ name: line.slice(0, separator), value: line.slice(separator + 1) }];
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function monotonicNow(globalObject) {
|
|
514
|
+
return globalObject.performance && typeof globalObject.performance.now === "function"
|
|
515
|
+
? globalObject.performance.now()
|
|
516
|
+
: Date.now();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function emitRecord(record, options, globalObject, capture) {
|
|
520
|
+
const sanitized = sanitizeNetworkRequest(record, options, globalObject);
|
|
521
|
+
if (!sanitized) return;
|
|
522
|
+
try {
|
|
523
|
+
const result = capture(sanitized);
|
|
524
|
+
if (result && typeof result.catch === "function") result.catch(() => undefined);
|
|
525
|
+
} catch (_) {
|
|
526
|
+
// Observability must not affect application networking.
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function installNetworkCapture({
|
|
531
|
+
globalObject = global,
|
|
532
|
+
options = {},
|
|
533
|
+
sourcePrefix = "react-native",
|
|
534
|
+
capture,
|
|
535
|
+
}) {
|
|
536
|
+
if (typeof capture !== "function") {
|
|
537
|
+
throw new TypeError("installNetworkCapture requires a capture callback.");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const cleanups = [];
|
|
541
|
+
let active = true;
|
|
542
|
+
let fetchInvocationDepth = 0;
|
|
543
|
+
|
|
544
|
+
if (options.captureFetch !== false && typeof globalObject.fetch === "function") {
|
|
545
|
+
const originalFetch = globalObject.fetch;
|
|
546
|
+
const wrappedFetch = function ansightFetch(input, init) {
|
|
547
|
+
const startedAtUtc = new Date().toISOString();
|
|
548
|
+
const started = monotonicNow(globalObject);
|
|
549
|
+
const inputHeaders = input && typeof input === "object" ? input.headers : undefined;
|
|
550
|
+
const requestHeaders = headerEntries(inputHeaders).concat(headerEntries(init && init.headers));
|
|
551
|
+
const requestBody = bodyFromValue(init && init.body, requestHeaders, options, globalObject);
|
|
552
|
+
const method = (init && init.method) || (input && input.method) || "GET";
|
|
553
|
+
const url = typeof input === "string" ? input : input && input.url ? input.url : String(input);
|
|
554
|
+
let promise;
|
|
555
|
+
fetchInvocationDepth += 1;
|
|
556
|
+
try {
|
|
557
|
+
promise = originalFetch.apply(this, arguments);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
fetchInvocationDepth -= 1;
|
|
560
|
+
if (active) emitRecord(
|
|
561
|
+
{
|
|
562
|
+
source: `${sourcePrefix}.fetch`,
|
|
563
|
+
startedAtUtc,
|
|
564
|
+
completedAtUtc: new Date().toISOString(),
|
|
565
|
+
durationMilliseconds: monotonicNow(globalObject) - started,
|
|
566
|
+
method,
|
|
567
|
+
url,
|
|
568
|
+
requestHeaders,
|
|
569
|
+
requestBodySizeBytes: parseContentLength(requestHeaders) ?? (requestBody && requestBody.totalBytes),
|
|
570
|
+
requestBody,
|
|
571
|
+
errorType: error && error.name,
|
|
572
|
+
errorMessage: error && error.message ? error.message : String(error),
|
|
573
|
+
},
|
|
574
|
+
options,
|
|
575
|
+
globalObject,
|
|
576
|
+
capture,
|
|
577
|
+
);
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
fetchInvocationDepth -= 1;
|
|
581
|
+
return Promise.resolve(promise).then(
|
|
582
|
+
(response) => {
|
|
583
|
+
if (!active) return response;
|
|
584
|
+
const responseHeaders = headerEntries(response && response.headers);
|
|
585
|
+
const responseRecord = {
|
|
586
|
+
source: `${sourcePrefix}.fetch`,
|
|
587
|
+
startedAtUtc,
|
|
588
|
+
completedAtUtc: new Date().toISOString(),
|
|
589
|
+
durationMilliseconds: monotonicNow(globalObject) - started,
|
|
590
|
+
method,
|
|
591
|
+
url: (response && response.url) || url,
|
|
592
|
+
requestHeaders,
|
|
593
|
+
requestBodySizeBytes: parseContentLength(requestHeaders) ?? (requestBody && requestBody.totalBytes),
|
|
594
|
+
requestBody,
|
|
595
|
+
statusCode: response && response.status,
|
|
596
|
+
reasonPhrase: response && response.statusText,
|
|
597
|
+
responseHeaders,
|
|
598
|
+
responseBodySizeBytes: parseContentLength(responseHeaders),
|
|
599
|
+
};
|
|
600
|
+
return bodyFromFetchResponse(
|
|
601
|
+
response,
|
|
602
|
+
responseHeaders,
|
|
603
|
+
options,
|
|
604
|
+
globalObject,
|
|
605
|
+
() => active,
|
|
606
|
+
).then(
|
|
607
|
+
(responseBody) => {
|
|
608
|
+
if (active) emitRecord(
|
|
609
|
+
{ ...responseRecord, responseBody, responseBodySizeBytes: responseRecord.responseBodySizeBytes ?? (responseBody && responseBody.totalBytes) },
|
|
610
|
+
options,
|
|
611
|
+
globalObject,
|
|
612
|
+
capture,
|
|
613
|
+
);
|
|
614
|
+
return response;
|
|
615
|
+
},
|
|
616
|
+
() => {
|
|
617
|
+
if (active) emitRecord(responseRecord, options, globalObject, capture);
|
|
618
|
+
return response;
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
},
|
|
622
|
+
(error) => {
|
|
623
|
+
if (active) emitRecord(
|
|
624
|
+
{
|
|
625
|
+
source: `${sourcePrefix}.fetch`,
|
|
626
|
+
startedAtUtc,
|
|
627
|
+
completedAtUtc: new Date().toISOString(),
|
|
628
|
+
durationMilliseconds: monotonicNow(globalObject) - started,
|
|
629
|
+
method,
|
|
630
|
+
url,
|
|
631
|
+
requestHeaders,
|
|
632
|
+
requestBodySizeBytes: parseContentLength(requestHeaders) ?? (requestBody && requestBody.totalBytes),
|
|
633
|
+
requestBody,
|
|
634
|
+
errorType: error && error.name,
|
|
635
|
+
errorMessage: error && error.message ? error.message : String(error),
|
|
636
|
+
},
|
|
637
|
+
options,
|
|
638
|
+
globalObject,
|
|
639
|
+
capture,
|
|
640
|
+
);
|
|
641
|
+
throw error;
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
};
|
|
645
|
+
globalObject.fetch = wrappedFetch;
|
|
646
|
+
cleanups.push(() => {
|
|
647
|
+
if (globalObject.fetch === wrappedFetch) globalObject.fetch = originalFetch;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const Xhr = globalObject.XMLHttpRequest;
|
|
652
|
+
if (options.captureXmlHttpRequest !== false && Xhr && Xhr.prototype) {
|
|
653
|
+
const states = new WeakMap();
|
|
654
|
+
const prototype = Xhr.prototype;
|
|
655
|
+
const originalOpen = prototype.open;
|
|
656
|
+
const originalSend = prototype.send;
|
|
657
|
+
const originalSetRequestHeader = prototype.setRequestHeader;
|
|
658
|
+
|
|
659
|
+
function wrappedOpen(method, url) {
|
|
660
|
+
states.set(this, {
|
|
661
|
+
method: method || "GET",
|
|
662
|
+
url: String(url),
|
|
663
|
+
requestHeaders: [],
|
|
664
|
+
suppressed: fetchInvocationDepth > 0,
|
|
665
|
+
});
|
|
666
|
+
return originalOpen.apply(this, arguments);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function wrappedSetRequestHeader(name, value) {
|
|
670
|
+
const state = states.get(this);
|
|
671
|
+
if (state) state.requestHeaders.push({ name, value });
|
|
672
|
+
return originalSetRequestHeader.apply(this, arguments);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function wrappedSend() {
|
|
676
|
+
const xhr = this;
|
|
677
|
+
const state = states.get(xhr);
|
|
678
|
+
if (!state || state.suppressed) return originalSend.apply(xhr, arguments);
|
|
679
|
+
state.startedAtUtc = new Date().toISOString();
|
|
680
|
+
state.started = monotonicNow(globalObject);
|
|
681
|
+
state.requestBody = bodyFromValue(arguments[0], state.requestHeaders, options, globalObject);
|
|
682
|
+
let failure;
|
|
683
|
+
const markFailure = (event) => {
|
|
684
|
+
failure = event && event.type ? event.type : "error";
|
|
685
|
+
};
|
|
686
|
+
const complete = () => {
|
|
687
|
+
if (!active) return;
|
|
688
|
+
let responseHeaders = [];
|
|
689
|
+
try {
|
|
690
|
+
responseHeaders = parseXhrResponseHeaders(xhr.getAllResponseHeaders());
|
|
691
|
+
} catch (_) {
|
|
692
|
+
// Some React Native XHR implementations throw before response headers exist.
|
|
693
|
+
}
|
|
694
|
+
let responseBody;
|
|
695
|
+
try {
|
|
696
|
+
const responseType = xhr.responseType || "text";
|
|
697
|
+
if (responseType === "text" || responseType === "") {
|
|
698
|
+
responseBody = bodyFromValue(xhr.responseText, responseHeaders, { ...options, captureRequestBody: options.captureResponseBody }, globalObject);
|
|
699
|
+
} else if (responseType === "arraybuffer") {
|
|
700
|
+
responseBody = bodyFromValue(xhr.response, responseHeaders, { ...options, captureRequestBody: options.captureResponseBody }, globalObject);
|
|
701
|
+
}
|
|
702
|
+
} catch (_) {
|
|
703
|
+
// Response data is not readable for every XHR response type.
|
|
704
|
+
}
|
|
705
|
+
emitRecord(
|
|
706
|
+
{
|
|
707
|
+
source: `${sourcePrefix}.xhr`,
|
|
708
|
+
startedAtUtc: state.startedAtUtc,
|
|
709
|
+
completedAtUtc: new Date().toISOString(),
|
|
710
|
+
durationMilliseconds: monotonicNow(globalObject) - state.started,
|
|
711
|
+
method: state.method,
|
|
712
|
+
url: xhr.responseURL || state.url,
|
|
713
|
+
requestHeaders: state.requestHeaders,
|
|
714
|
+
requestBodySizeBytes: parseContentLength(state.requestHeaders) ?? (state.requestBody && state.requestBody.totalBytes),
|
|
715
|
+
requestBody: state.requestBody,
|
|
716
|
+
statusCode: xhr.status || undefined,
|
|
717
|
+
reasonPhrase: xhr.statusText,
|
|
718
|
+
responseHeaders,
|
|
719
|
+
responseBodySizeBytes: parseContentLength(responseHeaders) ?? (responseBody && responseBody.totalBytes),
|
|
720
|
+
responseBody,
|
|
721
|
+
errorType: failure,
|
|
722
|
+
errorMessage: failure ? `XMLHttpRequest ${failure}` : undefined,
|
|
723
|
+
},
|
|
724
|
+
options,
|
|
725
|
+
globalObject,
|
|
726
|
+
capture,
|
|
727
|
+
);
|
|
728
|
+
};
|
|
729
|
+
xhr.addEventListener("error", markFailure);
|
|
730
|
+
xhr.addEventListener("abort", markFailure);
|
|
731
|
+
xhr.addEventListener("timeout", markFailure);
|
|
732
|
+
xhr.addEventListener("loadend", complete, { once: true });
|
|
733
|
+
return originalSend.apply(xhr, arguments);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
prototype.open = wrappedOpen;
|
|
737
|
+
prototype.send = wrappedSend;
|
|
738
|
+
prototype.setRequestHeader = wrappedSetRequestHeader;
|
|
739
|
+
cleanups.push(() => {
|
|
740
|
+
if (prototype.open === wrappedOpen) prototype.open = originalOpen;
|
|
741
|
+
if (prototype.send === wrappedSend) prototype.send = originalSend;
|
|
742
|
+
if (prototype.setRequestHeader === wrappedSetRequestHeader) {
|
|
743
|
+
prototype.setRequestHeader = originalSetRequestHeader;
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let removed = false;
|
|
749
|
+
return {
|
|
750
|
+
remove() {
|
|
751
|
+
if (removed) return;
|
|
752
|
+
removed = true;
|
|
753
|
+
active = false;
|
|
754
|
+
for (const cleanup of cleanups.reverse()) cleanup();
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
module.exports = {
|
|
760
|
+
REDACTED_VALUE,
|
|
761
|
+
SCHEMA,
|
|
762
|
+
installNetworkCapture,
|
|
763
|
+
sanitizeNetworkRequest,
|
|
764
|
+
};
|