@clue-ai/browser-sdk 0.0.1

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 (102) hide show
  1. package/README.md +100 -0
  2. package/dist/authoring/overlay.d.ts +12 -0
  3. package/dist/authoring/overlay.js +468 -0
  4. package/dist/authoring/recording.d.ts +125 -0
  5. package/dist/authoring/recording.js +481 -0
  6. package/dist/authoring/service-logo.d.ts +1 -0
  7. package/dist/authoring/service-logo.generated.d.ts +1 -0
  8. package/dist/authoring/service-logo.generated.js +3 -0
  9. package/dist/authoring/service-logo.js +1 -0
  10. package/dist/authoring/session.d.ts +23 -0
  11. package/dist/authoring/session.js +127 -0
  12. package/dist/authoring/surface.d.ts +11 -0
  13. package/dist/authoring/surface.js +63 -0
  14. package/dist/authoring/toolbar-constants.d.ts +23 -0
  15. package/dist/authoring/toolbar-constants.js +42 -0
  16. package/dist/authoring/toolbar-drag.d.ts +29 -0
  17. package/dist/authoring/toolbar-drag.js +270 -0
  18. package/dist/authoring/toolbar-view.d.ts +21 -0
  19. package/dist/authoring/toolbar-view.js +2584 -0
  20. package/dist/capture/action.d.ts +2 -0
  21. package/dist/capture/action.js +62 -0
  22. package/dist/capture/dom.d.ts +23 -0
  23. package/dist/capture/dom.js +329 -0
  24. package/dist/capture/drag.d.ts +2 -0
  25. package/dist/capture/drag.js +75 -0
  26. package/dist/capture/error.d.ts +2 -0
  27. package/dist/capture/error.js +193 -0
  28. package/dist/capture/form.d.ts +2 -0
  29. package/dist/capture/form.js +137 -0
  30. package/dist/capture/frustration.d.ts +2 -0
  31. package/dist/capture/frustration.js +171 -0
  32. package/dist/capture/input.d.ts +2 -0
  33. package/dist/capture/input.js +109 -0
  34. package/dist/capture/location.d.ts +10 -0
  35. package/dist/capture/location.js +42 -0
  36. package/dist/capture/navigation.d.ts +2 -0
  37. package/dist/capture/navigation.js +100 -0
  38. package/dist/capture/network.d.ts +13 -0
  39. package/dist/capture/network.js +903 -0
  40. package/dist/capture/page.d.ts +2 -0
  41. package/dist/capture/page.js +78 -0
  42. package/dist/capture/performance.d.ts +2 -0
  43. package/dist/capture/performance.js +268 -0
  44. package/dist/context/account.d.ts +12 -0
  45. package/dist/context/account.js +129 -0
  46. package/dist/context/environment.d.ts +42 -0
  47. package/dist/context/environment.js +208 -0
  48. package/dist/context/identity.d.ts +14 -0
  49. package/dist/context/identity.js +123 -0
  50. package/dist/context/session.d.ts +28 -0
  51. package/dist/context/session.js +155 -0
  52. package/dist/context/tab.d.ts +22 -0
  53. package/dist/context/tab.js +142 -0
  54. package/dist/context/trace.d.ts +32 -0
  55. package/dist/context/trace.js +65 -0
  56. package/dist/core/config.d.ts +4 -0
  57. package/dist/core/config.js +199 -0
  58. package/dist/core/constants.d.ts +43 -0
  59. package/dist/core/constants.js +109 -0
  60. package/dist/core/contracts.d.ts +58 -0
  61. package/dist/core/contracts.js +53 -0
  62. package/dist/core/sdk.d.ts +2 -0
  63. package/dist/core/sdk.js +831 -0
  64. package/dist/core/types.d.ts +413 -0
  65. package/dist/core/types.js +1 -0
  66. package/dist/core/usage-governor.d.ts +7 -0
  67. package/dist/core/usage-governor.js +127 -0
  68. package/dist/index.d.ts +17 -0
  69. package/dist/index.js +36 -0
  70. package/dist/integrations/next-router.d.ts +16 -0
  71. package/dist/integrations/next-router.js +18 -0
  72. package/dist/integrations/react-router.d.ts +7 -0
  73. package/dist/integrations/react-router.js +37 -0
  74. package/dist/internal/metrics.d.ts +9 -0
  75. package/dist/internal/metrics.js +38 -0
  76. package/dist/normalize/builders.d.ts +15 -0
  77. package/dist/normalize/builders.js +786 -0
  78. package/dist/normalize/canonical.d.ts +13 -0
  79. package/dist/normalize/canonical.js +77 -0
  80. package/dist/normalize/event-id.d.ts +8 -0
  81. package/dist/normalize/event-id.js +39 -0
  82. package/dist/normalize/path-template.d.ts +1 -0
  83. package/dist/normalize/path-template.js +33 -0
  84. package/dist/privacy/local-minimization.d.ts +29 -0
  85. package/dist/privacy/local-minimization.js +88 -0
  86. package/dist/privacy/mask.d.ts +7 -0
  87. package/dist/privacy/mask.js +60 -0
  88. package/dist/privacy/parameter-snapshot.d.ts +14 -0
  89. package/dist/privacy/parameter-snapshot.js +206 -0
  90. package/dist/privacy/sanitize.d.ts +11 -0
  91. package/dist/privacy/sanitize.js +145 -0
  92. package/dist/privacy/schema-evidence.d.ts +20 -0
  93. package/dist/privacy/schema-evidence.js +238 -0
  94. package/dist/transport/batch.d.ts +37 -0
  95. package/dist/transport/batch.js +182 -0
  96. package/dist/transport/client.d.ts +61 -0
  97. package/dist/transport/client.js +267 -0
  98. package/dist/transport/queue.d.ts +22 -0
  99. package/dist/transport/queue.js +56 -0
  100. package/dist/transport/retry.d.ts +14 -0
  101. package/dist/transport/retry.js +46 -0
  102. package/package.json +38 -0
@@ -0,0 +1,903 @@
1
+ import { generateEventId, generateRequestId, generateRequestSpanId, } from "../normalize/event-id";
2
+ import { toPathTemplate } from "../normalize/path-template";
3
+ import { buildLocalQuerySummary, buildLocalStructuredSummary, } from "../privacy/local-minimization";
4
+ import { maybeParseJson, sanitizeHeaders } from "../privacy/sanitize";
5
+ import { buildSchemaEvidence, deriveOutcomeClass, extractErrorCodeCandidate, extractGraphqlEvidence, } from "../privacy/schema-evidence";
6
+ import { hasAuthoringInternalRequestHeader, isAuthoringInternalRequestUrl, logAuthoringRequestExclusion, } from "../authoring/surface";
7
+ import { CLUE_SDK_REQUEST_HEADER } from "../transport/client";
8
+ const XHR_META_KEY = "__clue_xhr_meta__";
9
+ const headersToRecord = (headers) => {
10
+ if (!headers) {
11
+ return {};
12
+ }
13
+ if (headers instanceof Headers) {
14
+ const record = {};
15
+ headers.forEach((value, key) => {
16
+ record[key] = value;
17
+ });
18
+ return record;
19
+ }
20
+ if (Array.isArray(headers)) {
21
+ const record = {};
22
+ for (const [key, value] of headers) {
23
+ record[key] = value;
24
+ }
25
+ return record;
26
+ }
27
+ const record = {};
28
+ for (const [key, value] of Object.entries(headers)) {
29
+ record[key] = String(value);
30
+ }
31
+ return record;
32
+ };
33
+ const normalizeBody = (body) => {
34
+ if (body == null) {
35
+ return null;
36
+ }
37
+ if (typeof body === "string") {
38
+ return maybeParseJson(body) ?? body;
39
+ }
40
+ if (body instanceof URLSearchParams) {
41
+ const asObject = {};
42
+ body.forEach((value, key) => {
43
+ const current = asObject[key];
44
+ if (Array.isArray(current)) {
45
+ current.push(value);
46
+ }
47
+ else if (typeof current === "string") {
48
+ asObject[key] = [current, value];
49
+ }
50
+ else {
51
+ asObject[key] = value;
52
+ }
53
+ });
54
+ return asObject;
55
+ }
56
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
57
+ const asObject = {};
58
+ body.forEach((value, key) => {
59
+ const normalizedValue = typeof value === "string"
60
+ ? value
61
+ : {
62
+ _type: "file",
63
+ mime_type: value.type || null,
64
+ size: value.size,
65
+ };
66
+ const current = asObject[key];
67
+ if (Array.isArray(current)) {
68
+ current.push(normalizedValue);
69
+ }
70
+ else if (current !== undefined) {
71
+ asObject[key] = [current, normalizedValue];
72
+ }
73
+ else {
74
+ asObject[key] = normalizedValue;
75
+ }
76
+ });
77
+ return asObject;
78
+ }
79
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
80
+ return {
81
+ _type: "blob",
82
+ mime_type: body.type || null,
83
+ size: body.size,
84
+ };
85
+ }
86
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
87
+ return {
88
+ _type: "array_buffer",
89
+ byte_length: body.byteLength,
90
+ };
91
+ }
92
+ if (ArrayBuffer.isView(body)) {
93
+ return {
94
+ _type: "array_buffer_view",
95
+ byte_length: body.byteLength,
96
+ };
97
+ }
98
+ if (typeof body === "object") {
99
+ return body;
100
+ }
101
+ return null;
102
+ };
103
+ const resolveUrlAgainstCurrentLocation = (value) => {
104
+ try {
105
+ return new URL(value);
106
+ }
107
+ catch { }
108
+ const currentHref = typeof globalThis.location?.href === "string" &&
109
+ globalThis.location.href.length > 0
110
+ ? globalThis.location.href
111
+ : null;
112
+ if (!currentHref) {
113
+ return null;
114
+ }
115
+ try {
116
+ return new URL(value, currentHref);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ };
122
+ const stripQueryFromUrl = (url) => {
123
+ try {
124
+ const resolvedUrl = resolveUrlAgainstCurrentLocation(url);
125
+ if (!resolvedUrl) {
126
+ return url.split("?")[0] ?? url;
127
+ }
128
+ return `${resolvedUrl.origin}${resolvedUrl.pathname}`;
129
+ }
130
+ catch {
131
+ return url.split("?")[0] ?? url;
132
+ }
133
+ };
134
+ const buildParameterSource = (input) => {
135
+ const source = {};
136
+ if (input.headers && Object.keys(input.headers).length > 0) {
137
+ source.headers = input.headers;
138
+ }
139
+ if (input.query && Object.keys(input.query).length > 0) {
140
+ source.query = input.query;
141
+ }
142
+ if (input.body !== null && input.body !== undefined) {
143
+ source.body = input.body;
144
+ }
145
+ return source;
146
+ };
147
+ const buildRequestShapeEvidence = (input) => {
148
+ const requestSchemaSource = buildParameterSource({
149
+ query: input.queryValues,
150
+ body: input.normalizedRequestBody,
151
+ });
152
+ const requestSchemaEvidence = buildSchemaEvidence(Object.keys(requestSchemaSource).length > 0 ? requestSchemaSource : null);
153
+ const graphqlEvidence = extractGraphqlEvidence(input.normalizedRequestBody);
154
+ return {
155
+ requestKind: graphqlEvidence.requestKind ?? input.defaultRequestKind,
156
+ requestSchemaHash: requestSchemaEvidence.schemaHash,
157
+ requestFieldPaths: requestSchemaEvidence.fieldPaths,
158
+ requestShapeSize: requestSchemaEvidence.shapeSize,
159
+ graphqlOperationName: graphqlEvidence.graphqlOperationName,
160
+ graphqlOperationType: graphqlEvidence.graphqlOperationType,
161
+ graphqlTopLevelFields: graphqlEvidence.graphqlTopLevelFields,
162
+ };
163
+ };
164
+ const buildResponseShapeEvidence = (input) => {
165
+ const responseSchemaEvidence = buildSchemaEvidence(input.normalizedResponseBody);
166
+ return {
167
+ outcomeClass: deriveOutcomeClass({
168
+ statusCode: input.statusCode,
169
+ failureType: input.failureType,
170
+ responseBody: input.normalizedResponseBody,
171
+ }),
172
+ errorCodeCandidate: extractErrorCodeCandidate(input.normalizedResponseBody),
173
+ responseSchemaHash: responseSchemaEvidence.schemaHash,
174
+ responseFieldPaths: responseSchemaEvidence.fieldPaths,
175
+ responseShapeSize: responseSchemaEvidence.shapeSize,
176
+ };
177
+ };
178
+ const resolveRequestShape = (rawUrl) => {
179
+ const resolvedUrl = resolveUrlAgainstCurrentLocation(rawUrl);
180
+ const path = resolvedUrl?.pathname ?? "/";
181
+ return {
182
+ path,
183
+ pathTemplate: toPathTemplate(path),
184
+ targetService: resolvedUrl?.host ?? null,
185
+ };
186
+ };
187
+ const readQueryValues = (rawUrl) => {
188
+ try {
189
+ const resolvedUrl = resolveUrlAgainstCurrentLocation(rawUrl);
190
+ if (!resolvedUrl) {
191
+ return {};
192
+ }
193
+ const queryValues = {};
194
+ resolvedUrl.searchParams.forEach((value, key) => {
195
+ const current = queryValues[key];
196
+ if (current === undefined) {
197
+ queryValues[key] = value;
198
+ return;
199
+ }
200
+ if (Array.isArray(current)) {
201
+ queryValues[key] = [...current, value];
202
+ return;
203
+ }
204
+ queryValues[key] = [current, value];
205
+ });
206
+ return queryValues;
207
+ }
208
+ catch {
209
+ return {};
210
+ }
211
+ };
212
+ const estimateBodySizeBytes = (body) => {
213
+ if (body == null) {
214
+ return undefined;
215
+ }
216
+ if (typeof body === "string") {
217
+ return new TextEncoder().encode(body).byteLength;
218
+ }
219
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
220
+ return body.size;
221
+ }
222
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
223
+ return body.byteLength;
224
+ }
225
+ if (ArrayBuffer.isView(body)) {
226
+ return body.byteLength;
227
+ }
228
+ try {
229
+ return new TextEncoder().encode(JSON.stringify(body)).byteLength;
230
+ }
231
+ catch {
232
+ return undefined;
233
+ }
234
+ };
235
+ const deriveFailureTypeFromStatus = (status) => {
236
+ if (status >= 500) {
237
+ return "server_error";
238
+ }
239
+ if (status >= 400) {
240
+ return "client_error";
241
+ }
242
+ return null;
243
+ };
244
+ const PRIVACY_POLICY_VERSION = 1;
245
+ const hasSdkTransportHeader = (headers) => {
246
+ return typeof headers[CLUE_SDK_REQUEST_HEADER] === "string";
247
+ };
248
+ const shouldEmitImportantFailureResponse = (statusCode, failureType, isImportant4xxPath) => {
249
+ if (statusCode >= 500) {
250
+ return true;
251
+ }
252
+ if (statusCode >= 400 && statusCode < 500 && isImportant4xxPath) {
253
+ return true;
254
+ }
255
+ if (!failureType) {
256
+ return false;
257
+ }
258
+ return (failureType === "server_error" ||
259
+ failureType === "network_error" ||
260
+ failureType === "xhr_error" ||
261
+ failureType === "xhr_abort" ||
262
+ failureType === "xhr_timeout" ||
263
+ failureType === "fetch_error");
264
+ };
265
+ const readResponseBody = async (response) => {
266
+ const contentType = response.headers.get("content-type") || "";
267
+ if (!contentType.includes("application/json")) {
268
+ return null;
269
+ }
270
+ try {
271
+ const text = await response.text();
272
+ return maybeParseJson(text);
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ };
278
+ const getXhrResponseBody = (xhr) => {
279
+ try {
280
+ if (xhr.responseType === "" || xhr.responseType === "text") {
281
+ return maybeParseJson(xhr.responseText);
282
+ }
283
+ if (xhr.responseType === "json") {
284
+ return xhr.response;
285
+ }
286
+ return null;
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ };
292
+ const parseXhrHeaders = (rawHeaders) => {
293
+ const headers = {};
294
+ rawHeaders
295
+ .trim()
296
+ .split(/\r?\n/)
297
+ .forEach((line) => {
298
+ const separatorIndex = line.indexOf(":");
299
+ if (separatorIndex <= 0) {
300
+ return;
301
+ }
302
+ const key = line.slice(0, separatorIndex).trim();
303
+ const value = line.slice(separatorIndex + 1).trim();
304
+ if (key) {
305
+ headers[key] = value;
306
+ }
307
+ });
308
+ return headers;
309
+ };
310
+ export const startNetworkCapture = (context, options = {}) => {
311
+ const ignoreUrlPrefixes = options.ignoreUrlPrefixes ?? [];
312
+ const tracePropagationOrigins = new Set((options.tracePropagationOrigins ?? []).map((origin) => origin.trim().toLowerCase()));
313
+ const important4xxPathPrefixes = (options.important4xxPathPrefixes ?? [])
314
+ .map((prefix) => prefix.trim())
315
+ .filter(Boolean)
316
+ .map((prefix) => (prefix.startsWith("/") ? prefix : `/${prefix}`));
317
+ const createRequestTraceContext = options.createRequestTraceContext ??
318
+ (() => ({
319
+ traceId: `trace_${generateEventId()}`,
320
+ parentSpanId: null,
321
+ requestId: generateRequestId(),
322
+ requestSpanId: generateRequestSpanId(),
323
+ interactionId: null,
324
+ }));
325
+ const toTraceHeaders = (traceContext) => {
326
+ const parentSpanId = traceContext.parentSpanId ?? traceContext.requestId;
327
+ const observationContext = context.getObservationContext?.() ?? null;
328
+ const headers = {
329
+ "x-clue-trace-id": traceContext.traceId,
330
+ "x-clue-parent-span-id": parentSpanId,
331
+ "x-clue-request-id": traceContext.requestId,
332
+ "x-clue-request-span-id": traceContext.requestSpanId,
333
+ };
334
+ if (traceContext.interactionId) {
335
+ headers["x-clue-interaction-id"] = traceContext.interactionId;
336
+ }
337
+ if (observationContext?.sessionId) {
338
+ headers["x-clue-session-id"] = observationContext.sessionId;
339
+ }
340
+ if (observationContext?.anonymousId) {
341
+ headers["x-clue-anonymous-id"] = observationContext.anonymousId;
342
+ }
343
+ if (observationContext?.userId) {
344
+ headers["x-clue-user-id"] = observationContext.userId;
345
+ }
346
+ if (observationContext?.accountId) {
347
+ headers["x-clue-account-id"] = observationContext.accountId;
348
+ }
349
+ if (observationContext?.organizationId) {
350
+ headers["x-clue-organization-id"] = observationContext.organizationId;
351
+ }
352
+ if (observationContext?.tabId) {
353
+ headers["x-clue-tab-id"] = observationContext.tabId;
354
+ }
355
+ return headers;
356
+ };
357
+ const enrichFetchInitWithTraceHeaders = (init, request, traceHeaders) => {
358
+ const headers = new Headers(request?.headers);
359
+ if (init?.headers) {
360
+ new Headers(init.headers).forEach((value, key) => {
361
+ headers.set(key, value);
362
+ });
363
+ }
364
+ for (const [key, value] of Object.entries(traceHeaders)) {
365
+ headers.set(key, value);
366
+ }
367
+ return {
368
+ ...(init ?? {}),
369
+ headers,
370
+ };
371
+ };
372
+ const shouldIgnoreUrl = (url) => {
373
+ if (!url) {
374
+ return false;
375
+ }
376
+ return ignoreUrlPrefixes.some((prefix) => prefix && url.startsWith(prefix));
377
+ };
378
+ const shouldInjectTraceHeaders = (url) => {
379
+ if (!url) {
380
+ return false;
381
+ }
382
+ try {
383
+ const resolvedUrl = resolveUrlAgainstCurrentLocation(url);
384
+ if (!resolvedUrl) {
385
+ return false;
386
+ }
387
+ const requestOrigin = resolvedUrl.origin.toLowerCase();
388
+ const currentOrigin = globalThis.location?.origin?.toLowerCase() ?? null;
389
+ if (currentOrigin && requestOrigin === currentOrigin) {
390
+ return true;
391
+ }
392
+ return tracePropagationOrigins.has(requestOrigin);
393
+ }
394
+ catch {
395
+ return false;
396
+ }
397
+ };
398
+ const parseImportantPathTemplate = (url) => {
399
+ try {
400
+ const resolvedUrl = resolveUrlAgainstCurrentLocation(url);
401
+ if (!resolvedUrl) {
402
+ return null;
403
+ }
404
+ const pathTemplate = toPathTemplate(resolvedUrl.pathname);
405
+ return important4xxPathPrefixes.some((prefix) => pathTemplate.startsWith(prefix))
406
+ ? pathTemplate
407
+ : null;
408
+ }
409
+ catch {
410
+ return null;
411
+ }
412
+ };
413
+ const hasFetch = typeof globalThis.fetch === "function";
414
+ const hasXhr = typeof globalThis.XMLHttpRequest !== "undefined";
415
+ const originalFetch = hasFetch
416
+ ? globalThis.fetch.bind(globalThis)
417
+ : undefined;
418
+ if (hasFetch && originalFetch) {
419
+ globalThis.fetch = (async (input, init) => {
420
+ if (context.getConsent() === "denied") {
421
+ return originalFetch(input, init);
422
+ }
423
+ const startedAtMs = Date.now();
424
+ const request = input instanceof Request ? input : null;
425
+ const method = init?.method || (request?.method ? request.method : "GET");
426
+ const rawUrl = typeof input === "string"
427
+ ? input
428
+ : input instanceof URL
429
+ ? input.toString()
430
+ : input.url;
431
+ const url = stripQueryFromUrl(rawUrl);
432
+ const requestShape = resolveRequestShape(rawUrl);
433
+ if (shouldIgnoreUrl(rawUrl)) {
434
+ if (isAuthoringInternalRequestUrl(rawUrl)) {
435
+ logAuthoringRequestExclusion("ignore_url_prefix");
436
+ }
437
+ return originalFetch(input, init);
438
+ }
439
+ const traceContext = createRequestTraceContext();
440
+ const parentSpanId = traceContext.parentSpanId ?? traceContext.requestId;
441
+ const traceHeaders = toTraceHeaders(traceContext);
442
+ const enrichedInit = shouldInjectTraceHeaders(rawUrl)
443
+ ? enrichFetchInitWithTraceHeaders(init, request, traceHeaders)
444
+ : { ...(init ?? {}) };
445
+ const requestId = traceContext.requestId;
446
+ const requestSpanId = traceContext.requestSpanId;
447
+ const interactionId = traceContext.interactionId;
448
+ const requestHeaders = {
449
+ ...headersToRecord(request?.headers),
450
+ ...headersToRecord(enrichedInit.headers),
451
+ };
452
+ if (hasSdkTransportHeader(requestHeaders)) {
453
+ return originalFetch(input, init);
454
+ }
455
+ if (hasAuthoringInternalRequestHeader(requestHeaders)) {
456
+ logAuthoringRequestExclusion("request_header");
457
+ return originalFetch(input, init);
458
+ }
459
+ const requestBodyRaw = enrichedInit.body ?? null;
460
+ const normalizedRequestBody = normalizeBody(requestBodyRaw);
461
+ const queryValues = readQueryValues(rawUrl);
462
+ const queryKeys = Object.keys(queryValues);
463
+ const querySummary = buildLocalQuerySummary(queryValues);
464
+ const requestParametersSource = buildParameterSource({
465
+ headers: sanitizeHeaders(requestHeaders),
466
+ query: queryValues,
467
+ body: normalizedRequestBody,
468
+ });
469
+ const requestShapeEvidence = buildRequestShapeEvidence({
470
+ queryValues,
471
+ normalizedRequestBody,
472
+ defaultRequestKind: "fetch",
473
+ });
474
+ const requestSizeBytes = estimateBodySizeBytes(requestBodyRaw);
475
+ const requestMetadataSummary = buildLocalStructuredSummary({
476
+ headers: requestHeaders,
477
+ query: querySummary,
478
+ });
479
+ try {
480
+ const response = await originalFetch(input, enrichedInit);
481
+ const durationMs = Date.now() - startedAtMs;
482
+ const responseHeaders = headersToRecord(response.headers);
483
+ const normalizedResponseBody = await readResponseBody(response.clone());
484
+ const responseParametersSource = buildParameterSource({
485
+ headers: sanitizeHeaders(responseHeaders),
486
+ body: normalizedResponseBody,
487
+ });
488
+ const responseShapeEvidence = buildResponseShapeEvidence({
489
+ normalizedResponseBody,
490
+ statusCode: response.status,
491
+ });
492
+ const contentLength = response.headers.get("content-length");
493
+ const responseSizeBytes = contentLength && !Number.isNaN(Number(contentLength))
494
+ ? Number(contentLength)
495
+ : undefined;
496
+ const failureType = deriveFailureTypeFromStatus(response.status);
497
+ const important4xxPathTemplate = response.status >= 400 && response.status < 500
498
+ ? parseImportantPathTemplate(rawUrl)
499
+ : null;
500
+ if (shouldEmitImportantFailureResponse(response.status, failureType, Boolean(important4xxPathTemplate))) {
501
+ const occurredAtMs = Date.now();
502
+ context.onEvent({
503
+ type: "request_finished",
504
+ occurredAtMs,
505
+ payload: {
506
+ viewId: context.getViewId(),
507
+ requestId,
508
+ requestSpanId,
509
+ interactionId,
510
+ method,
511
+ url,
512
+ path: requestShape.path,
513
+ pathTemplate: requestShape.pathTemplate,
514
+ targetService: requestShape.targetService,
515
+ traceId: traceContext.traceId,
516
+ parentSpanId,
517
+ statusCode: response.status,
518
+ durationMs,
519
+ ...requestShapeEvidence,
520
+ ...responseShapeEvidence,
521
+ requestSizeBytes,
522
+ responseSizeBytes,
523
+ queryKeys,
524
+ requestMetadataSummary,
525
+ responseMetadataSummary: buildLocalStructuredSummary({
526
+ headers: responseHeaders,
527
+ }),
528
+ requestParametersSource,
529
+ responseParametersSource,
530
+ pageUrl: globalThis.location?.href ?? null,
531
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
532
+ },
533
+ });
534
+ return response;
535
+ }
536
+ context.onEvent({
537
+ type: "request_finished",
538
+ occurredAtMs: Date.now(),
539
+ payload: {
540
+ viewId: context.getViewId(),
541
+ requestId,
542
+ requestSpanId,
543
+ interactionId,
544
+ method,
545
+ url,
546
+ path: requestShape.path,
547
+ pathTemplate: requestShape.pathTemplate,
548
+ targetService: requestShape.targetService,
549
+ traceId: traceContext.traceId,
550
+ parentSpanId,
551
+ statusCode: response.status,
552
+ durationMs,
553
+ ...requestShapeEvidence,
554
+ ...responseShapeEvidence,
555
+ requestSizeBytes,
556
+ responseSizeBytes,
557
+ queryKeys,
558
+ requestMetadataSummary,
559
+ responseMetadataSummary: buildLocalStructuredSummary({
560
+ headers: responseHeaders,
561
+ }),
562
+ requestParametersSource,
563
+ responseParametersSource,
564
+ pageUrl: globalThis.location?.href ?? null,
565
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
566
+ },
567
+ });
568
+ return response;
569
+ }
570
+ catch (error) {
571
+ const durationMs = Date.now() - startedAtMs;
572
+ const failureType = error instanceof Error ? error.name || "fetch_error" : "fetch_error";
573
+ const responseShapeEvidence = buildResponseShapeEvidence({
574
+ normalizedResponseBody: null,
575
+ failureType,
576
+ });
577
+ context.onEvent({
578
+ type: "request_failed",
579
+ occurredAtMs: Date.now(),
580
+ payload: {
581
+ viewId: context.getViewId(),
582
+ requestId,
583
+ requestSpanId,
584
+ interactionId,
585
+ method,
586
+ url,
587
+ path: requestShape.path,
588
+ pathTemplate: requestShape.pathTemplate,
589
+ targetService: requestShape.targetService,
590
+ traceId: traceContext.traceId,
591
+ parentSpanId,
592
+ durationMs,
593
+ failureType,
594
+ ...requestShapeEvidence,
595
+ ...responseShapeEvidence,
596
+ queryKeys,
597
+ requestMetadataSummary,
598
+ requestParametersSource,
599
+ responseParametersSource: {},
600
+ pageUrl: globalThis.location?.href ?? null,
601
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
602
+ },
603
+ });
604
+ throw error;
605
+ }
606
+ });
607
+ }
608
+ let originalOpen = null;
609
+ let originalSend = null;
610
+ let originalSetRequestHeader = null;
611
+ if (hasXhr) {
612
+ originalOpen = XMLHttpRequest.prototype.open;
613
+ originalSend = XMLHttpRequest.prototype.send;
614
+ originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
615
+ XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
616
+ const xhr = this;
617
+ xhr[XHR_META_KEY] = {
618
+ method,
619
+ url: typeof url === "string" ? url : url.toString(),
620
+ requestHeaders: xhr[XHR_META_KEY]?.requestHeaders ?? {},
621
+ };
622
+ const open = originalOpen;
623
+ if (!open) {
624
+ return;
625
+ }
626
+ open.call(this, method, url, async ?? true, username ?? null, password ?? null);
627
+ };
628
+ XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
629
+ const xhr = this;
630
+ const meta = xhr[XHR_META_KEY] ?? {};
631
+ const requestHeaders = meta.requestHeaders ?? {};
632
+ requestHeaders[header] = value;
633
+ xhr[XHR_META_KEY] = {
634
+ ...meta,
635
+ requestHeaders,
636
+ };
637
+ const setRequestHeader = originalSetRequestHeader;
638
+ if (!setRequestHeader) {
639
+ return;
640
+ }
641
+ setRequestHeader.call(this, header, value);
642
+ };
643
+ XMLHttpRequest.prototype.send = function (body) {
644
+ const send = originalSend;
645
+ if (!send) {
646
+ return;
647
+ }
648
+ if (context.getConsent() === "denied") {
649
+ send.call(this, body);
650
+ return;
651
+ }
652
+ const xhr = this;
653
+ const previousMeta = xhr[XHR_META_KEY] ?? {};
654
+ const startedAtMs = Date.now();
655
+ const traceContext = createRequestTraceContext();
656
+ const parentSpanId = traceContext.parentSpanId ?? traceContext.requestId;
657
+ const requestId = traceContext.requestId;
658
+ const requestSpanId = traceContext.requestSpanId;
659
+ const interactionId = traceContext.interactionId;
660
+ const method = previousMeta.method ?? "GET";
661
+ const rawUrl = previousMeta.url ?? "";
662
+ const url = stripQueryFromUrl(rawUrl);
663
+ const requestShape = resolveRequestShape(rawUrl);
664
+ if (shouldIgnoreUrl(rawUrl)) {
665
+ if (isAuthoringInternalRequestUrl(rawUrl)) {
666
+ logAuthoringRequestExclusion("ignore_url_prefix");
667
+ }
668
+ send.call(this, body);
669
+ return;
670
+ }
671
+ const requestHeaders = previousMeta.requestHeaders ?? {};
672
+ if (hasSdkTransportHeader(requestHeaders)) {
673
+ send.call(this, body);
674
+ return;
675
+ }
676
+ if (hasAuthoringInternalRequestHeader(requestHeaders)) {
677
+ logAuthoringRequestExclusion("request_header");
678
+ send.call(this, body);
679
+ return;
680
+ }
681
+ if (shouldInjectTraceHeaders(rawUrl)) {
682
+ const traceHeaders = toTraceHeaders(traceContext);
683
+ for (const [header, value] of Object.entries(traceHeaders)) {
684
+ if (!requestHeaders[header]) {
685
+ xhr.setRequestHeader(header, value);
686
+ }
687
+ }
688
+ }
689
+ const normalizedRequestBody = normalizeBody(body);
690
+ const queryValues = readQueryValues(rawUrl);
691
+ const queryKeys = Object.keys(queryValues);
692
+ const querySummary = buildLocalQuerySummary(queryValues);
693
+ const requestParametersSource = buildParameterSource({
694
+ headers: sanitizeHeaders(requestHeaders),
695
+ query: queryValues,
696
+ body: normalizedRequestBody,
697
+ });
698
+ const requestShapeEvidence = buildRequestShapeEvidence({
699
+ queryValues,
700
+ normalizedRequestBody,
701
+ defaultRequestKind: "xhr",
702
+ });
703
+ const requestSizeBytes = estimateBodySizeBytes(body);
704
+ const viewId = context.getViewId();
705
+ const requestMetadataSummary = buildLocalStructuredSummary({
706
+ headers: requestHeaders,
707
+ query: querySummary,
708
+ });
709
+ const meta = {
710
+ method,
711
+ url,
712
+ requestHeaders,
713
+ startedAtMs,
714
+ requestId,
715
+ requestSpanId,
716
+ interactionId,
717
+ viewId,
718
+ };
719
+ xhr[XHR_META_KEY] = meta;
720
+ let failureEmitted = false;
721
+ const handleLoadEnd = () => {
722
+ const durationMs = Date.now() - startedAtMs;
723
+ const status = xhr.status;
724
+ if (status === 0) {
725
+ if (failureEmitted) {
726
+ return;
727
+ }
728
+ failureEmitted = true;
729
+ context.onEvent({
730
+ type: "request_failed",
731
+ occurredAtMs: Date.now(),
732
+ payload: {
733
+ viewId,
734
+ requestId,
735
+ requestSpanId,
736
+ interactionId,
737
+ method,
738
+ url,
739
+ path: requestShape.path,
740
+ pathTemplate: requestShape.pathTemplate,
741
+ targetService: requestShape.targetService,
742
+ traceId: traceContext.traceId,
743
+ parentSpanId,
744
+ durationMs,
745
+ failureType: "network_error",
746
+ ...requestShapeEvidence,
747
+ ...buildResponseShapeEvidence({
748
+ normalizedResponseBody: null,
749
+ failureType: "network_error",
750
+ }),
751
+ queryKeys,
752
+ requestMetadataSummary,
753
+ requestParametersSource,
754
+ responseParametersSource: {},
755
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
756
+ },
757
+ });
758
+ return;
759
+ }
760
+ const responseHeaders = parseXhrHeaders(xhr.getAllResponseHeaders());
761
+ const normalizedResponseBody = getXhrResponseBody(xhr);
762
+ const responseParametersSource = buildParameterSource({
763
+ headers: sanitizeHeaders(responseHeaders),
764
+ body: normalizedResponseBody,
765
+ });
766
+ const responseShapeEvidence = buildResponseShapeEvidence({
767
+ normalizedResponseBody,
768
+ statusCode: status,
769
+ });
770
+ const failureType = deriveFailureTypeFromStatus(status);
771
+ const important4xxPathTemplate = status >= 400 && status < 500
772
+ ? parseImportantPathTemplate(rawUrl)
773
+ : null;
774
+ if (shouldEmitImportantFailureResponse(status, failureType, Boolean(important4xxPathTemplate))) {
775
+ const occurredAtMs = Date.now();
776
+ context.onEvent({
777
+ type: "request_finished",
778
+ occurredAtMs,
779
+ payload: {
780
+ viewId,
781
+ requestId,
782
+ requestSpanId,
783
+ interactionId,
784
+ method,
785
+ url,
786
+ path: requestShape.path,
787
+ pathTemplate: requestShape.pathTemplate,
788
+ targetService: requestShape.targetService,
789
+ traceId: traceContext.traceId,
790
+ parentSpanId,
791
+ statusCode: status,
792
+ durationMs,
793
+ ...requestShapeEvidence,
794
+ ...responseShapeEvidence,
795
+ requestSizeBytes,
796
+ queryKeys,
797
+ requestMetadataSummary,
798
+ responseMetadataSummary: buildLocalStructuredSummary({
799
+ headers: responseHeaders,
800
+ }),
801
+ requestParametersSource,
802
+ responseParametersSource,
803
+ pageUrl: globalThis.location?.href ?? null,
804
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
805
+ },
806
+ });
807
+ return;
808
+ }
809
+ context.onEvent({
810
+ type: "request_finished",
811
+ occurredAtMs: Date.now(),
812
+ payload: {
813
+ viewId,
814
+ requestId,
815
+ requestSpanId,
816
+ interactionId,
817
+ method,
818
+ url,
819
+ path: requestShape.path,
820
+ pathTemplate: requestShape.pathTemplate,
821
+ targetService: requestShape.targetService,
822
+ traceId: traceContext.traceId,
823
+ parentSpanId,
824
+ statusCode: status,
825
+ durationMs,
826
+ ...requestShapeEvidence,
827
+ ...responseShapeEvidence,
828
+ requestSizeBytes,
829
+ queryKeys,
830
+ requestMetadataSummary,
831
+ responseMetadataSummary: buildLocalStructuredSummary({
832
+ headers: responseHeaders,
833
+ }),
834
+ requestParametersSource,
835
+ responseParametersSource,
836
+ pageUrl: globalThis.location?.href ?? null,
837
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
838
+ },
839
+ });
840
+ };
841
+ const handleFailure = (failureType) => {
842
+ if (failureEmitted) {
843
+ return;
844
+ }
845
+ failureEmitted = true;
846
+ const durationMs = Date.now() - startedAtMs;
847
+ context.onEvent({
848
+ type: "request_failed",
849
+ occurredAtMs: Date.now(),
850
+ payload: {
851
+ viewId,
852
+ requestId,
853
+ requestSpanId,
854
+ interactionId,
855
+ method,
856
+ url,
857
+ path: requestShape.path,
858
+ pathTemplate: requestShape.pathTemplate,
859
+ targetService: requestShape.targetService,
860
+ traceId: traceContext.traceId,
861
+ parentSpanId,
862
+ durationMs,
863
+ failureType,
864
+ ...requestShapeEvidence,
865
+ ...buildResponseShapeEvidence({
866
+ normalizedResponseBody: null,
867
+ failureType,
868
+ }),
869
+ queryKeys,
870
+ requestMetadataSummary,
871
+ requestParametersSource,
872
+ responseParametersSource: {},
873
+ pageUrl: globalThis.location?.href ?? null,
874
+ privacyPolicyVersion: PRIVACY_POLICY_VERSION,
875
+ },
876
+ });
877
+ };
878
+ xhr.addEventListener("loadend", handleLoadEnd, { once: true });
879
+ xhr.addEventListener("error", () => handleFailure("xhr_error"), {
880
+ once: true,
881
+ });
882
+ xhr.addEventListener("abort", () => handleFailure("xhr_abort"), {
883
+ once: true,
884
+ });
885
+ xhr.addEventListener("timeout", () => handleFailure("xhr_timeout"), {
886
+ once: true,
887
+ });
888
+ send.call(this, body);
889
+ };
890
+ }
891
+ return {
892
+ stop: () => {
893
+ if (hasFetch && originalFetch) {
894
+ globalThis.fetch = originalFetch;
895
+ }
896
+ if (hasXhr && originalOpen && originalSend && originalSetRequestHeader) {
897
+ XMLHttpRequest.prototype.open = originalOpen;
898
+ XMLHttpRequest.prototype.send = originalSend;
899
+ XMLHttpRequest.prototype.setRequestHeader = originalSetRequestHeader;
900
+ }
901
+ },
902
+ };
903
+ };