@reticlehq/browser 3.8.0 → 3.8.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.
@@ -1,42 +1,21 @@
1
- import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, ResponseBodyReason, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
1
+ import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, ResponseBodyReason, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
2
2
  import { captureMethod } from '../patching/capture-method.js';
3
3
  import { observeSafely, observeValue } from './types.js';
4
- import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, skippedBodyReason, withBodyDeadline, } from './net-detail/network-body.js';
4
+ import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, skippedBodyReason, } from './net-detail/network-body.js';
5
+ import { captureResponseBody, isEventStream } from './net-detail/network-response-body.js';
5
6
  import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
6
7
  import { watchStreamedBody } from './network-stream.js';
8
+ import { frameFields, isBridgeSocket } from './net-detail/network-frames.js';
7
9
  import { requireCapturedMethod } from '../util/captured-method.js';
8
10
  export { redactUrl, netUrlFields };
9
- function binaryFrameBytes(data) {
10
- if (data instanceof ArrayBuffer)
11
- return data.byteLength;
12
- if (typeof Blob !== 'undefined' && data instanceof Blob)
13
- return data.size;
14
- if (ArrayBuffer.isView(data))
15
- return data.byteLength;
16
- return undefined;
17
- }
18
- function frameFields(data, captureBodies) {
19
- if (typeof data !== 'string') {
20
- const bytes = binaryFrameBytes(data);
21
- return bytes === undefined
22
- ? { frameType: typeof data }
23
- : { frameType: 'binary', frameBytes: bytes };
24
- }
25
- const out = { frameBytes: data.length };
26
- if (captureBodies) {
27
- const { body, truncated } = projectBody(data, 'application/json');
28
- out['frame'] = body;
29
- if (truncated)
30
- out['frameTruncated'] = true;
31
- }
32
- return out;
33
- }
34
11
  const ShapeToken = { NULL: '0', STRING: 's', NUMBER: 'n', BOOLEAN: 'b', OTHER: '?' };
35
12
  function shapeOf(value) {
36
- if (null === value)
13
+ if (null === value) {
37
14
  return ShapeToken.NULL;
38
- if (Array.isArray(value))
15
+ }
16
+ if (Array.isArray(value)) {
39
17
  return `[${value.map(shapeOf).join(',')}]`;
18
+ }
40
19
  if ('object' === typeof value) {
41
20
  const fields = value;
42
21
  return `{${Object.keys(fields)
@@ -44,12 +23,15 @@ function shapeOf(value) {
44
23
  .map((key) => `${key}:${shapeOf(fields[key])}`)
45
24
  .join(',')}}`;
46
25
  }
47
- if ('string' === typeof value)
26
+ if ('string' === typeof value) {
48
27
  return ShapeToken.STRING;
49
- if ('number' === typeof value)
28
+ }
29
+ if ('number' === typeof value) {
50
30
  return ShapeToken.NUMBER;
51
- if ('boolean' === typeof value)
31
+ }
32
+ if ('boolean' === typeof value) {
52
33
  return ShapeToken.BOOLEAN;
34
+ }
53
35
  return ShapeToken.OTHER;
54
36
  }
55
37
  function fingerprintBody(shape, length) {
@@ -72,8 +54,9 @@ const DELETE = 0x7f;
72
54
  function hasControlCharacter(text) {
73
55
  for (let i = 0; i < text.length; i += 1) {
74
56
  const code = text.charCodeAt(i);
75
- if (code === DELETE)
57
+ if (code === DELETE) {
76
58
  return true;
59
+ }
77
60
  if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
78
61
  return true;
79
62
  }
@@ -81,10 +64,12 @@ function hasControlCharacter(text) {
81
64
  return false;
82
65
  }
83
66
  function textOfBytes(body) {
84
- if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
67
+ if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body))) {
85
68
  return undefined;
86
- if (body.byteLength > MAX_DECODED_BODY_BYTES)
69
+ }
70
+ if (body.byteLength > MAX_DECODED_BODY_BYTES) {
87
71
  return undefined;
72
+ }
88
73
  try {
89
74
  const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
90
75
  return hasControlCharacter(text) ? undefined : text;
@@ -97,8 +82,9 @@ function formShape(form) {
97
82
  const fields = [];
98
83
  let length = 0;
99
84
  form.forEach((value, name) => {
100
- if ('string' === typeof value)
85
+ if ('string' === typeof value) {
101
86
  length += value.length;
87
+ }
102
88
  fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
103
89
  });
104
90
  return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
@@ -111,8 +97,9 @@ function projectRequestBody(body, captureBodies) {
111
97
  const typeOf = () => body.constructor?.name ?? typeof body;
112
98
  if ('string' === typeof body || bytesText !== undefined) {
113
99
  text = bytesText ?? body;
114
- if (text.length > 0)
100
+ if (text.length > 0) {
115
101
  shape = fingerprintBody(jsonShape(text), text.length);
102
+ }
116
103
  }
117
104
  else if (typeof FormData !== 'undefined' && body instanceof FormData) {
118
105
  const form = formShape(body);
@@ -133,8 +120,9 @@ function projectRequestBody(body, captureBodies) {
133
120
  return captureBodies ? { requestBodyType: typeOf() } : {};
134
121
  }
135
122
  const fields = { [REQUEST_SHAPE_FIELD]: shape };
136
- if (!captureBodies || text === undefined || 0 === text.length)
123
+ if (!captureBodies || text === undefined || 0 === text.length) {
137
124
  return fields;
125
+ }
138
126
  const { body: out, truncated } = projectBody(text, contentType);
139
127
  return truncated
140
128
  ? { ...fields, requestBody: out, requestBodyTruncated: true }
@@ -145,27 +133,34 @@ function statusIsOk(status) {
145
133
  }
146
134
  function netResponseMeta(statusText, contentType, contentLength) {
147
135
  const out = {};
148
- if (statusText !== '')
136
+ if (statusText !== '') {
149
137
  out['statusText'] = statusText;
150
- if (contentType !== null && contentType !== '')
138
+ }
139
+ if (contentType !== null && contentType !== '') {
151
140
  out['contentType'] = contentType;
141
+ }
152
142
  const size = contentLength !== null ? Number.parseInt(contentLength, 10) : Number.NaN;
153
- if (Number.isFinite(size))
143
+ if (Number.isFinite(size)) {
154
144
  out['responseSize'] = size;
145
+ }
155
146
  return out;
156
147
  }
157
148
  function urlOf(input) {
158
- if ('string' === typeof input)
149
+ if ('string' === typeof input) {
159
150
  return input;
160
- if (input instanceof URL)
151
+ }
152
+ if (input instanceof URL) {
161
153
  return input.href;
154
+ }
162
155
  return input.url;
163
156
  }
164
157
  function methodOf(input, init) {
165
- if (init?.method !== undefined)
158
+ if (init?.method !== undefined) {
166
159
  return init.method.toUpperCase();
167
- if (input instanceof Request)
160
+ }
161
+ if (input instanceof Request) {
168
162
  return input.method.toUpperCase();
163
+ }
169
164
  return 'GET';
170
165
  }
171
166
  function nextActionOf(input, init) {
@@ -180,8 +175,9 @@ function nextActionOf(input, init) {
180
175
  const NON_APP_FRAME = /@reticlehq|reticle\.ts|network\.ts|transport\.ts|<anonymous>|new Promise|node:internal/i;
181
176
  const DEPENDENCY_FRAME = /node_modules|\/\.vite\/deps\//i;
182
177
  export function firstAppFrame(stack) {
183
- if (stack === undefined)
178
+ if (stack === undefined) {
184
179
  return undefined;
180
+ }
185
181
  const frames = stack
186
182
  .split('\n')
187
183
  .slice(1)
@@ -194,14 +190,16 @@ function initiatorFrame() {
194
190
  return firstAppFrame(new Error().stack);
195
191
  }
196
192
  export function extractTiming(entry) {
197
- if (entry === undefined)
193
+ if (entry === undefined) {
198
194
  return {};
195
+ }
199
196
  const timing = {};
200
197
  if (entry.responseStart > 0 && entry.requestStart > 0) {
201
198
  timing.ttfbMs = Math.round(entry.responseStart - entry.requestStart);
202
199
  }
203
- if (entry.transferSize > 0)
200
+ if (entry.transferSize > 0) {
204
201
  timing.transferSize = entry.transferSize;
202
+ }
205
203
  return timing;
206
204
  }
207
205
  function resourceTiming(rawUrl) {
@@ -213,14 +211,6 @@ function resourceTiming(rawUrl) {
213
211
  return {};
214
212
  }
215
213
  }
216
- function isBridgeSocket(url) {
217
- try {
218
- return new URL(url, location.href).pathname === RETICLE_WS_PATH;
219
- }
220
- catch {
221
- return url.includes(RETICLE_WS_PATH);
222
- }
223
- }
224
214
  function isNativeFetch(fn) {
225
215
  try {
226
216
  return Function.prototype.toString.call(fn).includes('native code');
@@ -233,11 +223,13 @@ const OURS = new WeakSet();
233
223
  const PAGE_CAPTURE = Symbol.for('reticle.network');
234
224
  const pageSlots = () => window;
235
225
  export function installNetwork(emit, opts = {}) {
236
- if (undefined !== pageSlots()[PAGE_CAPTURE])
226
+ if (undefined !== pageSlots()[PAGE_CAPTURE]) {
237
227
  return () => undefined;
228
+ }
238
229
  const owner = {};
239
230
  pageSlots()[PAGE_CAPTURE] = owner;
240
231
  const captureBodies = true === opts.captureBodies;
232
+ const bodyCapture = new AbortController();
241
233
  const captureErrorBodies = false !== opts.captureErrorBodies;
242
234
  const reinterpret = opts.reinterpret;
243
235
  const origFetch = requireCapturedMethod(window, 'fetch');
@@ -249,8 +241,9 @@ export function installNetwork(emit, opts = {}) {
249
241
  const nextId = () => `n${++seq}`;
250
242
  window.fetch = async (input, init) => {
251
243
  const rawUrl = urlOf(input);
252
- if (true === opts.ignore?.(rawUrl))
244
+ if (true === opts.ignore?.(rawUrl)) {
253
245
  return callFetch(input, init);
246
+ }
254
247
  const id = nextId();
255
248
  const start = performance.now();
256
249
  const method = methodOf(input, init);
@@ -299,40 +292,26 @@ export function installNetwork(emit, opts = {}) {
299
292
  });
300
293
  };
301
294
  const wantErrorBody = shouldCaptureErrorBody(res.status, captureErrorBodies);
302
- if ((captureBodies || wantErrorBody) && isCapturableType(contentType)) {
303
- let clone;
304
- try {
305
- clone = res.clone();
306
- }
307
- catch {
308
- }
309
- void (async () => {
310
- let responseBodyFields = captureBodies
311
- ? { responseBodyReason: ResponseBodyReason.UNREADABLE }
312
- : {};
313
- if (clone !== undefined) {
314
- try {
315
- const text = await withBodyDeadline(clone.text());
316
- if (text !== undefined) {
317
- const { body, truncated } = captureBodies
318
- ? projectBody(text, contentType)
319
- : projectErrorBody(text, contentType);
320
- responseBodyFields = {
321
- responseBody: body,
322
- ...(truncated ? { responseBodyTruncated: true } : {}),
323
- ...(captureBodies ? {} : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
324
- };
325
- }
326
- }
327
- catch {
328
- }
329
- }
330
- try {
331
- emitRequest(responseBodyFields);
332
- }
333
- catch {
295
+ if (captureBodies && isEventStream(contentType)) {
296
+ emitRequest({ responseBodyReason: ResponseBodyReason.UNREADABLE });
297
+ void captureResponseBody(res, contentType, true, bodyCapture.signal)
298
+ .then((fields) => {
299
+ if (typeof fields['responseBody'] !== 'string') {
300
+ return;
334
301
  }
335
- })().catch(() => undefined);
302
+ observeSafely(() => emit(EventType.NET_RESPONSE_BODY, {
303
+ id,
304
+ method,
305
+ ...urlFields,
306
+ ...fields,
307
+ }));
308
+ })
309
+ .catch(() => undefined);
310
+ }
311
+ else if ((captureBodies || wantErrorBody) && isCapturableType(contentType)) {
312
+ void captureResponseBody(res, contentType, captureBodies, bodyCapture.signal)
313
+ .then(emitRequest)
314
+ .catch(() => undefined);
336
315
  }
337
316
  else {
338
317
  const skipped = captureBodies ? skippedBodyReason(contentType) : undefined;
@@ -399,23 +378,25 @@ export function installNetwork(emit, opts = {}) {
399
378
  this.addEventListener('loadend', () => {
400
379
  observeSafely(() => {
401
380
  const cur = meta.get(this);
402
- if (cur === undefined)
381
+ if (cur === undefined) {
403
382
  return;
383
+ }
404
384
  reportedNetUrls.add(cur.rawUrl);
405
385
  const xhrContentType = this.getResponseHeader('content-type');
406
386
  let responseBodyFields = {};
407
387
  const textReadable = '' === this.responseType || 'text' === this.responseType;
408
388
  const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
389
+ const capturable = isCapturableType(xhrContentType) ||
390
+ (captureBodies && isEventStream(xhrContentType));
409
391
  if (captureBodies) {
410
- const skipped = isCapturableType(xhrContentType)
392
+ const skipped = capturable
411
393
  ? ResponseBodyReason.UNREADABLE
412
394
  : skippedBodyReason(xhrContentType);
413
- if (skipped !== undefined)
395
+ if (skipped !== undefined) {
414
396
  responseBodyFields = { responseBodyReason: skipped };
397
+ }
415
398
  }
416
- if ((captureBodies || wantXhrErrorBody) &&
417
- textReadable &&
418
- isCapturableType(xhrContentType)) {
399
+ if ((captureBodies || wantXhrErrorBody) && textReadable && capturable) {
419
400
  try {
420
401
  const { body: rb, truncated } = captureBodies
421
402
  ? projectBody(this.responseText, xhrContentType)
@@ -489,8 +470,9 @@ export function installNetwork(emit, opts = {}) {
489
470
  constructor(u, protocols) {
490
471
  super(u, protocols);
491
472
  this.#isBridge = isBridgeSocket(String(u));
492
- if (this.#isBridge)
473
+ if (this.#isBridge) {
493
474
  return;
475
+ }
494
476
  observeSafely(() => {
495
477
  const urlFields = netUrlFields(String(u));
496
478
  emit(EventType.NET_STREAM, {
@@ -564,14 +546,17 @@ export function installNetwork(emit, opts = {}) {
564
546
  const observer = new PerformanceObserver((list) => {
565
547
  for (const raw of list.getEntries()) {
566
548
  const entry = raw;
567
- if (subresourceEvents >= SUBRESOURCE_CAP)
549
+ if (subresourceEvents >= SUBRESOURCE_CAP) {
568
550
  return;
551
+ }
569
552
  const type = entry.initiatorType || 'other';
570
- if ('fetch' === type || 'xmlhttprequest' === type)
553
+ if ('fetch' === type || 'xmlhttprequest' === type) {
571
554
  continue;
555
+ }
572
556
  const rawUrl = entry.name;
573
- if (reportedNetUrls.has(rawUrl))
557
+ if (reportedNetUrls.has(rawUrl)) {
574
558
  continue;
559
+ }
575
560
  reportedNetUrls.add(rawUrl);
576
561
  subresourceEvents += 1;
577
562
  emit(EventType.NET_REQUEST, {
@@ -597,15 +582,20 @@ export function installNetwork(emit, opts = {}) {
597
582
  }
598
583
  }
599
584
  return () => {
600
- if (pageSlots()[PAGE_CAPTURE] === owner)
585
+ if (pageSlots()[PAGE_CAPTURE] === owner) {
601
586
  delete pageSlots()[PAGE_CAPTURE];
587
+ }
588
+ bodyCapture.abort();
602
589
  subresourceObserver?.disconnect();
603
- if (window.fetch === patchedFetch)
590
+ if (window.fetch === patchedFetch) {
604
591
  window.fetch = origFetch;
605
- if (captureMethod(proto, 'open') === patchedOpen)
592
+ }
593
+ if (captureMethod(proto, 'open') === patchedOpen) {
606
594
  proto.open = origOpen;
607
- if (captureMethod(proto, 'send') === patchedSend)
595
+ }
596
+ if (captureMethod(proto, 'send') === patchedSend) {
608
597
  proto.send = origSend;
598
+ }
609
599
  if (patchedEventSource !== undefined && window.EventSource === patchedEventSource) {
610
600
  window.EventSource = origEventSource;
611
601
  }