@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.
- package/dist/actions/action-report.js +36 -18
- package/dist/actions/actions.js +66 -36
- package/dist/actions/danger-context.js +27 -16
- package/dist/actions/scroll.d.ts +2 -0
- package/dist/actions/scroll.js +10 -2
- package/dist/dom/a11y.d.ts +0 -4
- package/dist/dom/a11y.js +2 -294
- package/dist/dom/dom-ignore.js +23 -10
- package/dist/dom/query-finders.d.ts +6 -0
- package/dist/dom/query-finders.js +216 -0
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +108 -244
- package/dist/dom/snapshot.js +2 -1
- package/dist/dom/visibility.d.ts +7 -0
- package/dist/dom/visibility.js +322 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/observers/dom.js +2 -1
- package/dist/observers/net-detail/network-body.js +29 -9
- package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
- package/dist/observers/net-detail/network-event-stream.js +55 -0
- package/dist/observers/net-detail/network-frames.d.ts +2 -0
- package/dist/observers/net-detail/network-frames.js +39 -0
- package/dist/observers/net-detail/network-response-body.d.ts +3 -0
- package/dist/observers/net-detail/network-response-body.js +97 -0
- package/dist/observers/network.js +95 -105
- package/dist/reticle-inject.js +695 -631
- package/package.json +2 -2
|
@@ -1,42 +1,21 @@
|
|
|
1
|
-
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
}
|
|
29
|
+
if ('number' === typeof value) {
|
|
50
30
|
return ShapeToken.NUMBER;
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
592
|
+
}
|
|
593
|
+
if (captureMethod(proto, 'open') === patchedOpen) {
|
|
606
594
|
proto.open = origOpen;
|
|
607
|
-
|
|
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
|
}
|