@buoy-gg/network 7.0.39 → 7.0.41
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 +6 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/network/components/NetworkListScreen.js +2 -3
- package/lib/commonjs/network/components/NetworkListScreen.web.js +2 -3
- package/lib/commonjs/network/components/NetworkModal.js +1 -1
- package/lib/commonjs/network/conditions/xhr.js +26 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +99 -42
- package/lib/commonjs/network/overrides/matchRule.js +7 -1
- package/lib/commonjs/network/overrides/overriddenRows.js +37 -0
- package/lib/commonjs/network/overrides/resolveOverride.js +1 -1
- package/lib/commonjs/network/utils/networkEventStore.js +24 -6
- package/lib/commonjs/network/utils/networkListener.js +106 -11
- package/lib/commonjs/network/utils/networkSavedStore.js +4 -4
- package/lib/commonjs/network/utils/requestId.js +16 -0
- package/lib/commonjs/network/utils/resolveRequestUrl.js +15 -0
- package/lib/commonjs/network/utils/startNetworkCapture.js +24 -0
- package/lib/commonjs/preset.js +3 -12
- package/lib/module/index.js +1 -0
- package/lib/module/network/components/NetworkListScreen.js +2 -3
- package/lib/module/network/components/NetworkListScreen.web.js +2 -3
- package/lib/module/network/components/NetworkModal.js +1 -1
- package/lib/module/network/conditions/xhr.js +27 -2
- package/lib/module/network/overrides/applyToXhr.js +98 -42
- package/lib/module/network/overrides/matchRule.js +7 -1
- package/lib/module/network/overrides/overriddenRows.js +33 -0
- package/lib/module/network/overrides/resolveOverride.js +1 -1
- package/lib/module/network/utils/networkEventStore.js +25 -7
- package/lib/module/network/utils/networkListener.js +106 -12
- package/lib/module/network/utils/networkSavedStore.js +4 -4
- package/lib/module/network/utils/requestId.js +12 -0
- package/lib/module/network/utils/resolveRequestUrl.js +11 -0
- package/lib/module/network/utils/startNetworkCapture.js +21 -0
- package/lib/module/preset.js +3 -12
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +1 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -1
- package/lib/typescript/network/overrides/overriddenRows.d.ts +16 -0
- package/lib/typescript/network/overrides/overriddenRows.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +3 -4
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +4 -4
- package/lib/typescript/network/utils/requestId.d.ts +2 -0
- package/lib/typescript/network/utils/requestId.d.ts.map +1 -0
- package/lib/typescript/network/utils/resolveRequestUrl.d.ts +3 -0
- package/lib/typescript/network/utils/resolveRequestUrl.d.ts.map +1 -0
- package/lib/typescript/network/utils/startNetworkCapture.d.ts +6 -0
- package/lib/typescript/network/utils/startNetworkCapture.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -1
- package/lib/web/index.mjs +146 -0
- package/package.json +32 -10
package/README.md
CHANGED
|
@@ -93,3 +93,9 @@ Each call keeps the profile selected when it starts. Offline takes precedence ov
|
|
|
93
93
|
Conditions remain active when the panel is closed or its list is paused. They reset on a full JS reload or account access loss and are never persisted. Captured requests retain their condition stamp for saved snapshots, detail views and export. Desktop and MCP can control a connected device that advertises the conditions actions.
|
|
94
94
|
|
|
95
95
|
Network conditions are a development preview. The native compatibility matrix across transports, runtimes and devices is still being worked through, so confirm the behaviour you rely on in your own build before you depend on it.
|
|
96
|
+
|
|
97
|
+
## Web support (unreleased)
|
|
98
|
+
|
|
99
|
+
Browser fetch and XHR use the shared capture, rules, conditions, and request panels. CORS still controls which response data the page can read. The browser build is available in this checkout and has not been published yet. See the [web setup guide](https://buoy.gg/buoy/latest/docs/web-preview) for registration, dependencies, and browser boundaries.
|
|
100
|
+
|
|
101
|
+
The capture control reads “Pause network capture” while capture is enabled and “Resume network capture” while it is paused. Existing requests remain visible while capture is paused.
|
package/lib/commonjs/index.js
CHANGED
|
@@ -243,6 +243,12 @@ Object.defineProperty(exports, "splitPinned", {
|
|
|
243
243
|
return _withPinnedFirst.splitPinned;
|
|
244
244
|
}
|
|
245
245
|
});
|
|
246
|
+
Object.defineProperty(exports, "startNetworkCapture", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function () {
|
|
249
|
+
return _startNetworkCapture.startNetworkCapture;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
246
252
|
Object.defineProperty(exports, "subscribeNetworkConditions", {
|
|
247
253
|
enumerable: true,
|
|
248
254
|
get: function () {
|
|
@@ -298,6 +304,7 @@ Object.defineProperty(exports, "useTickEveryMinute", {
|
|
|
298
304
|
}
|
|
299
305
|
});
|
|
300
306
|
var _preset = require("./preset");
|
|
307
|
+
var _startNetworkCapture = require("./network/utils/startNetworkCapture");
|
|
301
308
|
var _NetworkModal = require("./network/components/NetworkModal");
|
|
302
309
|
var _NetworkEventDetailView = require("./network/components/NetworkEventDetailView");
|
|
303
310
|
var _NetworkEventItemCompact = require("./network/components/NetworkEventItemCompact");
|
|
@@ -12,7 +12,7 @@ var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
|
|
|
12
12
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
13
13
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
14
14
|
var _useOverrideRules = require("../hooks/useOverrideRules");
|
|
15
|
-
var
|
|
15
|
+
var _overriddenRows = require("../overrides/overriddenRows");
|
|
16
16
|
var _withPinnedFirst = require("../utils/withPinnedFirst");
|
|
17
17
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
18
|
/**
|
|
@@ -196,8 +196,7 @@ function NetworkListScreen({
|
|
|
196
196
|
// Paused rules still populate the strip — that's how you find the thing you
|
|
197
197
|
// paused in order to resume it. The header says PAUSED instead.
|
|
198
198
|
const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
|
|
199
|
-
const
|
|
200
|
-
const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
|
|
199
|
+
const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
|
|
201
200
|
const overriddenIds = new Set(overridden.map(event => event.id));
|
|
202
201
|
const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
|
|
203
202
|
const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
|
|
@@ -9,7 +9,7 @@ var _NetworkEventItemCompact = require("./NetworkEventItemCompact.web");
|
|
|
9
9
|
var _useNetworkEventList = require("../hooks/useNetworkEventList");
|
|
10
10
|
var _useNetworkSaved = require("../hooks/useNetworkSaved");
|
|
11
11
|
var _useOverrideRules = require("../hooks/useOverrideRules");
|
|
12
|
-
var
|
|
12
|
+
var _overriddenRows = require("../overrides/overriddenRows");
|
|
13
13
|
var _withPinnedFirst = require("../utils/withPinnedFirst");
|
|
14
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
15
15
|
/**
|
|
@@ -387,8 +387,7 @@ function NetworkListScreen({
|
|
|
387
387
|
// Hoist anything an active rule covers. Done after `splitPinned` so a request
|
|
388
388
|
// that is both pinned and overridden appears once, in the louder section.
|
|
389
389
|
const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
|
|
390
|
-
const
|
|
391
|
-
const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
|
|
390
|
+
const overridden = (0, _overriddenRows.overriddenEvents)([...pinned, ...rest], activeRules);
|
|
392
391
|
const overriddenIds = new Set(overridden.map(event => event.id));
|
|
393
392
|
const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
|
|
394
393
|
const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
|
|
@@ -501,7 +501,7 @@ function NetworkModalInner({
|
|
|
501
501
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
502
502
|
isEnabled: !paused,
|
|
503
503
|
onToggle: () => setPaused(prev => !prev),
|
|
504
|
-
accessibilityLabel: "Pause network
|
|
504
|
+
accessibilityLabel: paused ? "Resume network capture" : "Pause network capture",
|
|
505
505
|
testID: "buoy-network-power"
|
|
506
506
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkClearButton, {
|
|
507
507
|
paused: paused
|
|
@@ -7,7 +7,32 @@ exports.runConditionXhr = runConditionXhr;
|
|
|
7
7
|
var _applyToXhr = require("../overrides/applyToXhr");
|
|
8
8
|
/** Active simulation uses a send-to-completion deadline, including the wait. */
|
|
9
9
|
function runConditionXhr(target, url, condition, outcome, dispatch) {
|
|
10
|
-
if (!(0, _applyToXhr.canSynthesize)(target))
|
|
10
|
+
if (!(0, _applyToXhr.canSynthesize)(target)) {
|
|
11
|
+
const rule = {
|
|
12
|
+
id: "condition",
|
|
13
|
+
enabled: true,
|
|
14
|
+
urlPattern: url,
|
|
15
|
+
kind: "delay",
|
|
16
|
+
hits: 0,
|
|
17
|
+
seen: 0,
|
|
18
|
+
createdAt: Date.now()
|
|
19
|
+
};
|
|
20
|
+
const simulation = condition.profile === "offline" ? {
|
|
21
|
+
kind: "fail",
|
|
22
|
+
rule,
|
|
23
|
+
failKind: "network",
|
|
24
|
+
delayMs: 0
|
|
25
|
+
} : outcome ? {
|
|
26
|
+
...outcome,
|
|
27
|
+
delayMs: condition.delayMs + outcome.delayMs
|
|
28
|
+
} : {
|
|
29
|
+
kind: "delay",
|
|
30
|
+
rule,
|
|
31
|
+
delayMs: condition.delayMs
|
|
32
|
+
};
|
|
33
|
+
if (!(0, _applyToXhr.runOverride)(target, url, simulation, dispatch)) throw new Error("Network conditions are unavailable for this XMLHttpRequest implementation.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
11
36
|
const xhr = target;
|
|
12
37
|
if (xhr.readyState !== 1 || xhr._sent) throw new Error("Request is not open or has already been sent.");
|
|
13
38
|
const started = Date.now();
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.canSynthesize = canSynthesize;
|
|
7
7
|
exports.canSynthesizeStandard = canSynthesizeStandard;
|
|
8
8
|
exports.completeNativeOverride = completeNativeOverride;
|
|
9
|
+
exports.hasPendingStandardRequest = hasPendingStandardRequest;
|
|
9
10
|
exports.runOverride = runOverride;
|
|
10
11
|
/**
|
|
11
12
|
* Perform an override outcome against an XMLHttpRequest — React Native's OR a
|
|
@@ -286,6 +287,7 @@ function respondStandard(xhr, url, outcome, isAborted) {
|
|
|
286
287
|
setStandardReadyState(xhr, LOADING);
|
|
287
288
|
if (isAborted()) return;
|
|
288
289
|
setStandardReadyState(xhr, DONE);
|
|
290
|
+
if (isAborted()) return;
|
|
289
291
|
fireEvent(xhr, "load");
|
|
290
292
|
fireEvent(xhr, "loadend");
|
|
291
293
|
}
|
|
@@ -314,60 +316,115 @@ function failStandard(xhr, outcome) {
|
|
|
314
316
|
* `delay` pass-through hands the request to the real network, where the
|
|
315
317
|
* prototype `abort()` must be reachable again.
|
|
316
318
|
*/
|
|
319
|
+
const pendingStandardRequests = new WeakMap();
|
|
320
|
+
function hasPendingStandardRequest(xhr) {
|
|
321
|
+
return pendingStandardRequests.has(xhr);
|
|
322
|
+
}
|
|
317
323
|
function runStandardOverride(xhr, url, outcome, passThrough) {
|
|
318
|
-
|
|
319
|
-
|
|
324
|
+
const started = Date.now();
|
|
325
|
+
const generation = {};
|
|
326
|
+
let timer;
|
|
327
|
+
let deadline;
|
|
320
328
|
let settled = false;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
let aborted = false;
|
|
330
|
+
let dispatched = false;
|
|
331
|
+
let timeout = xhr.timeout;
|
|
332
|
+
const originalAbort = xhr.abort;
|
|
333
|
+
const originalOpen = xhr.open;
|
|
334
|
+
const names = ['abort', 'open', 'timeout', 'readyState', 'status', 'statusText', 'response', 'responseText', 'responseURL', 'getAllResponseHeaders', 'getResponseHeader'];
|
|
335
|
+
const descriptors = new Map(names.map(name => [name, Object.getOwnPropertyDescriptor(xhr, name)]));
|
|
336
|
+
let prototype = Object.getPrototypeOf(xhr);
|
|
337
|
+
let nativeTimeout;
|
|
338
|
+
while (prototype && !nativeTimeout) {
|
|
339
|
+
nativeTimeout = Object.getOwnPropertyDescriptor(prototype, 'timeout');
|
|
340
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
341
|
+
}
|
|
342
|
+
const finish = () => {
|
|
343
|
+
settled = true;
|
|
344
|
+
if (pendingStandardRequests.get(xhr) === generation) pendingStandardRequests.delete(xhr);
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
clearTimeout(deadline);
|
|
347
|
+
xhr.removeEventListener('loadend', finish);
|
|
348
|
+
};
|
|
349
|
+
const restore = () => {
|
|
350
|
+
finish();
|
|
351
|
+
for (const [name, descriptor] of descriptors) {
|
|
352
|
+
if (descriptor) Object.defineProperty(xhr, name, descriptor);else delete xhr[name];
|
|
327
353
|
}
|
|
354
|
+
xhr.timeout = timeout;
|
|
328
355
|
};
|
|
329
|
-
|
|
356
|
+
const scheduleDeadline = () => {
|
|
357
|
+
clearTimeout(deadline);
|
|
358
|
+
if (dispatched) {
|
|
359
|
+
nativeTimeout?.set?.call(xhr, timeout > 0 ? Math.max(1, timeout - (Date.now() - started)) : 0);
|
|
360
|
+
} else if (timeout > 0) {
|
|
361
|
+
deadline = setTimeout(() => {
|
|
362
|
+
if (settled) return;
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
failStandard(xhr, {
|
|
365
|
+
...outcome,
|
|
366
|
+
kind: 'fail',
|
|
367
|
+
failKind: 'timeout'
|
|
368
|
+
});
|
|
369
|
+
finish();
|
|
370
|
+
}, Math.max(0, timeout - (Date.now() - started)));
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
Object.defineProperty(xhr, 'timeout', {
|
|
374
|
+
configurable: true,
|
|
375
|
+
get: () => timeout,
|
|
376
|
+
set: value => {
|
|
377
|
+
timeout = Math.max(0, Number(value) || 0);
|
|
378
|
+
scheduleDeadline();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
Object.defineProperty(xhr, 'open', {
|
|
382
|
+
configurable: true,
|
|
383
|
+
value: (...args) => {
|
|
384
|
+
restore();
|
|
385
|
+
return Reflect.apply(originalOpen, xhr, args);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
Object.defineProperty(xhr, 'abort', {
|
|
330
389
|
configurable: true,
|
|
331
|
-
writable: true,
|
|
332
390
|
value: () => {
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
timer = null;
|
|
391
|
+
if (dispatched || settled) {
|
|
392
|
+
restore();
|
|
393
|
+
originalAbort.call(xhr);
|
|
394
|
+
return;
|
|
339
395
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// whatwg's request-error steps, which is also what RN's setReadyState
|
|
343
|
-
// dispatches off `_aborted`.
|
|
396
|
+
aborted = true;
|
|
397
|
+
clearTimeout(timer);
|
|
344
398
|
setStandardReadyState(xhr, DONE);
|
|
345
|
-
fireEvent(xhr,
|
|
346
|
-
fireEvent(xhr,
|
|
399
|
+
fireEvent(xhr, 'abort');
|
|
400
|
+
fireEvent(xhr, 'loadend');
|
|
401
|
+
finish();
|
|
402
|
+
if (!pendingStandardRequests.has(xhr)) defineOwn(xhr, 'readyState', 0);
|
|
347
403
|
}
|
|
348
404
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
405
|
+
pendingStandardRequests.set(xhr, generation);
|
|
406
|
+
xhr.addEventListener('loadend', finish);
|
|
407
|
+
scheduleDeadline();
|
|
408
|
+
timer = setTimeout(() => {
|
|
409
|
+
if (settled || aborted) return;
|
|
410
|
+
if (outcome.kind === 'delay') {
|
|
411
|
+
dispatched = true;
|
|
412
|
+
scheduleDeadline();
|
|
413
|
+
try {
|
|
414
|
+
passThrough();
|
|
415
|
+
} catch {
|
|
416
|
+
failStandard(xhr, {
|
|
417
|
+
...outcome,
|
|
418
|
+
kind: 'fail',
|
|
419
|
+
failKind: 'network'
|
|
420
|
+
});
|
|
421
|
+
finish();
|
|
422
|
+
}
|
|
357
423
|
return;
|
|
358
424
|
}
|
|
359
|
-
if (outcome.kind ===
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
respondStandard(xhr, url, outcome, () => aborted);
|
|
363
|
-
}
|
|
364
|
-
settled = true;
|
|
365
|
-
restoreAbort();
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// ALWAYS asynchronous, even at 0ms — same contract as the RN path: callers
|
|
369
|
-
// legitimately attach handlers after `send()` returns (axios does).
|
|
370
|
-
timer = setTimeout(fire, outcome.delayMs);
|
|
425
|
+
if (outcome.kind === 'fail') failStandard(xhr, outcome);else respondStandard(xhr, url, outcome, () => aborted || pendingStandardRequests.get(xhr) !== generation);
|
|
426
|
+
finish();
|
|
427
|
+
}, outcome.delayMs);
|
|
371
428
|
return true;
|
|
372
429
|
}
|
|
373
430
|
|
|
@@ -57,7 +57,13 @@ function compilePattern(pattern) {
|
|
|
57
57
|
// Anchored, like Chrome's `^${...}$` — a pattern describes the whole URL, so
|
|
58
58
|
// `pokeapi.co` matches nothing and `*pokeapi.co*` matches everything on that
|
|
59
59
|
// host. Predictable beats clever; the rule editor writes full URLs for you.
|
|
60
|
-
|
|
60
|
+
//
|
|
61
|
+
// One exception: a pattern that starts with `/` is a PATH. Anchored to the
|
|
62
|
+
// full URL it could never match anything (every URL starts with a scheme),
|
|
63
|
+
// so it means "this path on any origin" — `/api/rewards` matches
|
|
64
|
+
// `http://localhost:3002/api/rewards`, and `/api/rewards*` also its queries.
|
|
65
|
+
const source = pattern.startsWith("/") ? `[a-zA-Z][a-zA-Z0-9+.-]*://[^/]*${escapeRegex(pattern)}` : escapeRegex(pattern);
|
|
66
|
+
const regex = new RegExp(`^${source}$`);
|
|
61
67
|
// Unbounded growth isn't possible in practice (patterns come from a capped
|
|
62
68
|
// rule list), but a user editing a pattern character by character churns
|
|
63
69
|
// entries, so keep the cache from becoming a leak.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.overriddenEvents = overriddenEvents;
|
|
7
|
+
var _matchRule = require("./matchRule");
|
|
8
|
+
/**
|
|
9
|
+
* Which captured requests belong in the list's OVERRIDDEN strip.
|
|
10
|
+
*
|
|
11
|
+
* A row belongs there when an active rule actually changed it (it carries the
|
|
12
|
+
* rule's override mark) or matched it after the rule existed (while overrides
|
|
13
|
+
* are paused those pass through, and the strip is how you find the rule again).
|
|
14
|
+
* Before a rule existed nothing overrode anything, so older matching requests
|
|
15
|
+
* are real server responses and stay in the list, with one exception: the
|
|
16
|
+
* newest of them, which is the request the rule was built from. That one hoists
|
|
17
|
+
* right away, so creating a rule shows where it points without waiting for the
|
|
18
|
+
* endpoint to be called again.
|
|
19
|
+
*/
|
|
20
|
+
function overriddenEvents(events, activeRules) {
|
|
21
|
+
if (activeRules.length === 0) return [];
|
|
22
|
+
const activeIds = new Set(activeRules.map(rule => rule.id));
|
|
23
|
+
const picked = new Set();
|
|
24
|
+
for (const rule of activeRules) {
|
|
25
|
+
let source;
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
if (event.override && activeIds.has(event.override.ruleId)) {
|
|
28
|
+
picked.add(event.id);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!(0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET"))) continue;
|
|
32
|
+
if (event.timestamp >= rule.createdAt) picked.add(event.id);else if (!source || event.timestamp > source.timestamp) source = event;
|
|
33
|
+
}
|
|
34
|
+
if (source) picked.add(source.id);
|
|
35
|
+
}
|
|
36
|
+
return events.filter(event => picked.has(event.id));
|
|
37
|
+
}
|
|
@@ -30,7 +30,7 @@ var _types = require("./types");
|
|
|
30
30
|
* "the dev tools stopped working and I don't know why". Metro/debugger traffic
|
|
31
31
|
* is already excluded upstream by `networkListener.shouldIgnoreUrl`.
|
|
32
32
|
*/
|
|
33
|
-
const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i];
|
|
33
|
+
const NEVER_OVERRIDE = [/(^|\.)keygen\.sh/i, /^https?:\/\/(www\.)?buoy\.gg\/api\/license(\/|$)/i];
|
|
34
34
|
function isProtectedUrl(url) {
|
|
35
35
|
return NEVER_OVERRIDE.some(pattern => pattern.test(url));
|
|
36
36
|
}
|
|
@@ -140,9 +140,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// If no other listeners exist, stop the network interceptor
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
listener.stopListening();
|
|
143
|
+
if ((0, _networkListener.getNetworkListenerCount)() === 0) {
|
|
144
|
+
(0, _networkListener.stopNetworkListener)();
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -155,9 +154,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
155
154
|
disableCapture() {
|
|
156
155
|
this.mirrorMode = true;
|
|
157
156
|
this.takeBootBuffer();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
listener.stopListening();
|
|
157
|
+
if ((0, _networkListener.getNetworkListenerCount)() === 0) {
|
|
158
|
+
(0, _networkListener.stopNetworkListener)();
|
|
161
159
|
}
|
|
162
160
|
super.disableCapture();
|
|
163
161
|
}
|
|
@@ -204,6 +202,8 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
204
202
|
request
|
|
205
203
|
} = event;
|
|
206
204
|
if (event.type === "request") {
|
|
205
|
+
// Replayed request notifications must not reset a completed row or add a second key.
|
|
206
|
+
if (this.events.some(item => item.id === request.id)) return;
|
|
207
207
|
// Build full URL with query params if present
|
|
208
208
|
const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
|
|
209
209
|
const fullUrl = `${request.url}${queryString}`;
|
|
@@ -242,6 +242,24 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
242
242
|
this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
|
|
243
243
|
this.notify(networkEvent);
|
|
244
244
|
this.notifyArrayListeners();
|
|
245
|
+
} else if (event.type === "request-body") {
|
|
246
|
+
const current = this.events.find(item => item.id === request.id);
|
|
247
|
+
if (!current) return;
|
|
248
|
+
const updated = {
|
|
249
|
+
...current,
|
|
250
|
+
requestData: request.data,
|
|
251
|
+
requestSize: this.getDataSize(request.data)
|
|
252
|
+
};
|
|
253
|
+
if (request.client === "graphql") {
|
|
254
|
+
updated.operationName = (0, _extractOperationName.extractOperationName)(request.data) || undefined;
|
|
255
|
+
const variables = request.data?.variables;
|
|
256
|
+
if (variables && typeof variables === "object") updated.graphqlVariables = variables;
|
|
257
|
+
}
|
|
258
|
+
this.events = this.events.map(item => item.id === request.id ? updated : item);
|
|
259
|
+
if (this.pendingRequests.has(request.id)) this.pendingRequests.set(request.id, updated);
|
|
260
|
+
_networkSavedStore.networkSavedStore.syncFromLive(updated);
|
|
261
|
+
this.notify(updated);
|
|
262
|
+
this.notifyArrayListeners();
|
|
245
263
|
} else if (event.type === "response" || event.type === "error") {
|
|
246
264
|
const index = this.events.findIndex(e => e.id === request.id);
|
|
247
265
|
if (index !== -1) {
|
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.isNetworkInterceptionLive = exports.getNetworkListenerCount = exports.addNetworkListener = exports.BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = void 0;
|
|
7
|
+
var _resolveRequestUrl = require("./resolveRequestUrl");
|
|
8
|
+
var _requestId = require("./requestId");
|
|
7
9
|
var _xhr = require("../conditions/xhr");
|
|
8
10
|
var _store = require("../conditions/store");
|
|
9
11
|
var _wait = require("../conditions/wait");
|
|
@@ -17,6 +19,59 @@ var _httpCompletion = require("./httpCompletion");
|
|
|
17
19
|
* Simple and reliable network interception for React Native
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
let devOrigin;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Headers another Buoy tool adds to a request inside our wrapper.
|
|
26
|
+
*
|
|
27
|
+
* Impersonate patches fetch/XHR at boot, so this listener usually wraps it
|
|
28
|
+
* and records the request before the impersonation header is added. The
|
|
29
|
+
* request on the wire carried the header and the Network row did not. Tools
|
|
30
|
+
* that add headers register a function here, keyed by URL, so the row shows
|
|
31
|
+
* what was actually sent.
|
|
32
|
+
*/
|
|
33
|
+
function injectedHeaders(url) {
|
|
34
|
+
const injectors = globalThis.__buoyHeaderInjectors;
|
|
35
|
+
if (!injectors || injectors.length === 0) return undefined;
|
|
36
|
+
let out;
|
|
37
|
+
for (const inject of injectors) {
|
|
38
|
+
try {
|
|
39
|
+
const h = inject(url);
|
|
40
|
+
if (h) out = {
|
|
41
|
+
...out,
|
|
42
|
+
...h
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
/* a broken injector must not break capture */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Origin of the Metro dev server this JS bundle was loaded from, read once from
|
|
53
|
+
* the bundle URL React Native reports. `null` in release builds (the bundle is
|
|
54
|
+
* a file) and anywhere react-native isn't present.
|
|
55
|
+
*/
|
|
56
|
+
function devServerOrigin() {
|
|
57
|
+
if (devOrigin !== undefined) return devOrigin;
|
|
58
|
+
devOrigin = null;
|
|
59
|
+
try {
|
|
60
|
+
if (typeof __DEV__ !== "undefined" && !__DEV__) return devOrigin;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
|
+
const rn = require("react-native");
|
|
63
|
+
const sc = rn?.NativeModules?.SourceCode;
|
|
64
|
+
const url = sc?.getConstants?.().scriptURL ?? sc?.scriptURL;
|
|
65
|
+
if (typeof url === "string" && /^https?:\/\//.test(url)) {
|
|
66
|
+
const u = new URL(url);
|
|
67
|
+
devOrigin = `${u.protocol}//${u.host}`;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
devOrigin = null;
|
|
71
|
+
}
|
|
72
|
+
return devOrigin;
|
|
73
|
+
}
|
|
74
|
+
|
|
20
75
|
/**
|
|
21
76
|
* True only while `originalFetch` is being CALLED (not awaited).
|
|
22
77
|
*
|
|
@@ -122,7 +177,6 @@ function isInternalBuoyHost(url) {
|
|
|
122
177
|
class NetworkListener {
|
|
123
178
|
listeners = [];
|
|
124
179
|
isListening = false;
|
|
125
|
-
requestCounter = 1000;
|
|
126
180
|
|
|
127
181
|
// Store original methods
|
|
128
182
|
|
|
@@ -166,8 +220,15 @@ class NetworkListener {
|
|
|
166
220
|
try {
|
|
167
221
|
const parsed = new URL(url);
|
|
168
222
|
if (parsed.hostname === "keygen.sh" || parsed.hostname.endsWith(".keygen.sh")) return true;
|
|
223
|
+
// Buoy's own license endpoint (7.0.39+): the same traffic, new address.
|
|
224
|
+
if ((parsed.hostname === "buoy.gg" || parsed.hostname === "www.buoy.gg") && parsed.pathname.startsWith("/api/license")) return true;
|
|
169
225
|
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "10.0.2.2";
|
|
170
226
|
if (local && ["8081", "8802"].includes(parsed.port)) return true;
|
|
227
|
+
// The dev server this bundle actually came from, whatever its host and
|
|
228
|
+
// port (a LAN address, --port 8082…): Metro's own traffic — symbolicate,
|
|
229
|
+
// logs, HMR, the bundle — is not the app's.
|
|
230
|
+
const dev = devServerOrigin();
|
|
231
|
+
if (dev && `${parsed.protocol}//${parsed.host}` === dev) return true;
|
|
171
232
|
} catch {
|
|
172
233
|
return false;
|
|
173
234
|
}
|
|
@@ -623,7 +684,7 @@ class NetworkListener {
|
|
|
623
684
|
this.originalFetch = globalThis.fetch;
|
|
624
685
|
const generation = ++this.fetchGeneration;
|
|
625
686
|
const wrappedFetch = async (input, init) => {
|
|
626
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
687
|
+
const url = (0, _resolveRequestUrl.resolveRequestUrl)(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
627
688
|
|
|
628
689
|
// A newer install supersedes this wrapper. If it is still stranded in
|
|
629
690
|
// another tool's chain it must pass through silently, or one request
|
|
@@ -637,7 +698,7 @@ class NetworkListener {
|
|
|
637
698
|
return passthroughFetch(input, init);
|
|
638
699
|
}
|
|
639
700
|
const startTime = Date.now();
|
|
640
|
-
const requestId =
|
|
701
|
+
const requestId = (0, _requestId.nextRequestId)("fetch");
|
|
641
702
|
const inputRequest = typeof input === "object" && "method" in input ? input : undefined;
|
|
642
703
|
const method = init?.method ?? inputRequest?.method ?? "GET";
|
|
643
704
|
const signal = init?.signal !== undefined ? init.signal : inputRequest?.signal;
|
|
@@ -661,9 +722,12 @@ class NetworkListener {
|
|
|
661
722
|
requestHeaders[key] = value;
|
|
662
723
|
});
|
|
663
724
|
} else {
|
|
664
|
-
requestHeaders =
|
|
725
|
+
requestHeaders = {
|
|
726
|
+
...headers
|
|
727
|
+
};
|
|
665
728
|
}
|
|
666
729
|
}
|
|
730
|
+
Object.assign(requestHeaders, injectedHeaders(url));
|
|
667
731
|
|
|
668
732
|
// Parse request body
|
|
669
733
|
// Explicitly typed: the late emit for a streamed reply reads this from
|
|
@@ -712,6 +776,35 @@ class NetworkListener {
|
|
|
712
776
|
override,
|
|
713
777
|
condition
|
|
714
778
|
});
|
|
779
|
+
|
|
780
|
+
// Clone before dispatch; inspect the body without delaying or consuming the app's upload.
|
|
781
|
+
if (init?.body === undefined && inputRequest?.body && !inputRequest.bodyUsed) {
|
|
782
|
+
try {
|
|
783
|
+
const bodyCopy = inputRequest.clone();
|
|
784
|
+
void self.readStreamedBody(bodyCopy).then(({
|
|
785
|
+
body
|
|
786
|
+
}) => {
|
|
787
|
+
try {
|
|
788
|
+
requestData = JSON.parse(body);
|
|
789
|
+
} catch {
|
|
790
|
+
requestData = body;
|
|
791
|
+
}
|
|
792
|
+
self.emit({
|
|
793
|
+
type: "request-body",
|
|
794
|
+
timestamp: new Date(),
|
|
795
|
+
request: {
|
|
796
|
+
id: requestId || "unknown",
|
|
797
|
+
url: cleanUrl,
|
|
798
|
+
method,
|
|
799
|
+
data: requestData,
|
|
800
|
+
client: clientType
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
}).catch(() => {});
|
|
804
|
+
} catch {
|
|
805
|
+
// An unreadable body must not prevent the app's request.
|
|
806
|
+
}
|
|
807
|
+
}
|
|
715
808
|
try {
|
|
716
809
|
if (condition || outcome) await (0, _wait.waitBeforeDispatch)((condition?.delayMs ?? 0) + (outcome?.delayMs ?? 0), signal);
|
|
717
810
|
(0, _wait.throwIfAborted)(signal);
|
|
@@ -910,9 +1003,9 @@ class NetworkListener {
|
|
|
910
1003
|
if (generation === self.xhrGeneration) {
|
|
911
1004
|
pendingCapture.get(this)?.();
|
|
912
1005
|
const xhr = this;
|
|
913
|
-
xhr._buoyRequestId =
|
|
1006
|
+
xhr._buoyRequestId = (0, _requestId.nextRequestId)("xhr");
|
|
914
1007
|
xhr._method = method;
|
|
915
|
-
xhr._url = url;
|
|
1008
|
+
xhr._url = (0, _resolveRequestUrl.resolveRequestUrl)(url);
|
|
916
1009
|
xhr._buoyStartTime = Date.now();
|
|
917
1010
|
xhr._requestHeaders = {};
|
|
918
1011
|
}
|
|
@@ -934,6 +1027,7 @@ class NetworkListener {
|
|
|
934
1027
|
if (generation !== self.xhrGeneration || insideFetchHandoff) {
|
|
935
1028
|
return passthroughSend.call(this, data);
|
|
936
1029
|
}
|
|
1030
|
+
if ((0, _applyToXhr.hasPendingStandardRequest)(this)) throw new DOMException("The request has already been sent.", "InvalidStateError");
|
|
937
1031
|
if (this.readyState !== 1 || this._sent) {
|
|
938
1032
|
return passthroughSend.call(this, data);
|
|
939
1033
|
}
|
|
@@ -945,6 +1039,7 @@ class NetworkListener {
|
|
|
945
1039
|
xhr._buoyStartTime = startTime;
|
|
946
1040
|
xhr._buoyTtfb = undefined;
|
|
947
1041
|
const requestHeaders = xhr._requestHeaders || {};
|
|
1042
|
+
Object.assign(requestHeaders, injectedHeaders(url));
|
|
948
1043
|
|
|
949
1044
|
// Skip ignored URLs
|
|
950
1045
|
if (self.shouldIgnoreUrl(url)) {
|
|
@@ -1207,7 +1302,7 @@ const startNetworkListener = () => getNetworkListener().startListening();
|
|
|
1207
1302
|
* Stop network traffic monitoring
|
|
1208
1303
|
*/
|
|
1209
1304
|
exports.startNetworkListener = startNetworkListener;
|
|
1210
|
-
const stopNetworkListener = () =>
|
|
1305
|
+
const stopNetworkListener = () => _networkListener?.stopListening();
|
|
1211
1306
|
|
|
1212
1307
|
/**
|
|
1213
1308
|
* Add a listener for network events
|
|
@@ -1232,7 +1327,7 @@ const addNetworkListener = listener => getNetworkListener().addListener(listener
|
|
|
1232
1327
|
* Remove all registered network event listeners
|
|
1233
1328
|
*/
|
|
1234
1329
|
exports.addNetworkListener = addNetworkListener;
|
|
1235
|
-
const removeAllNetworkListeners = () =>
|
|
1330
|
+
const removeAllNetworkListeners = () => _networkListener?.removeAllListeners();
|
|
1236
1331
|
|
|
1237
1332
|
/**
|
|
1238
1333
|
* Check if network monitoring is currently active
|
|
@@ -1240,7 +1335,7 @@ const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners(
|
|
|
1240
1335
|
* @returns True if currently intercepting network traffic
|
|
1241
1336
|
*/
|
|
1242
1337
|
exports.removeAllNetworkListeners = removeAllNetworkListeners;
|
|
1243
|
-
const isNetworkListening = () =>
|
|
1338
|
+
const isNetworkListening = () => _networkListener?.isActive ?? false;
|
|
1244
1339
|
|
|
1245
1340
|
/**
|
|
1246
1341
|
* Get the number of registered network event listeners
|
|
@@ -1248,7 +1343,7 @@ const isNetworkListening = () => getNetworkListener().isActive;
|
|
|
1248
1343
|
* @returns Number of active listeners
|
|
1249
1344
|
*/
|
|
1250
1345
|
exports.isNetworkListening = isNetworkListening;
|
|
1251
|
-
const getNetworkListenerCount = () =>
|
|
1346
|
+
const getNetworkListenerCount = () => _networkListener?.listenerCount ?? 0;
|
|
1252
1347
|
|
|
1253
1348
|
/**
|
|
1254
1349
|
* Whether both interceptors are actually reachable through the live global
|
|
@@ -1256,5 +1351,5 @@ const getNetworkListenerCount = () => getNetworkListener().listenerCount;
|
|
|
1256
1351
|
* even after another tool re-assigns `globalThis.fetch` over the wrapper).
|
|
1257
1352
|
*/
|
|
1258
1353
|
exports.getNetworkListenerCount = getNetworkListenerCount;
|
|
1259
|
-
const isNetworkInterceptionLive = () =>
|
|
1354
|
+
const isNetworkInterceptionLive = () => _networkListener?.isInterceptionLive() ?? false;
|
|
1260
1355
|
exports.isNetworkInterceptionLive = isNetworkInterceptionLive;
|
|
@@ -22,10 +22,10 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
22
22
|
* A record exists while either flag is true and is dropped when both go false,
|
|
23
23
|
* so all four states fall out without special cases.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* Request IDs include a runtime session and a counter. Saved records also
|
|
26
|
+
* track their owning session because live rows disappear on app reload.
|
|
27
|
+
* Records use a generated `key`, carry the `sessionId` that captured them,
|
|
28
|
+
* and only trust `liveId` while that
|
|
29
29
|
* session is the current one. Snapshots restored from an earlier session get
|
|
30
30
|
* their event `id` rewritten to `saved:<key>` so a restored pin can never
|
|
31
31
|
* resolve to (or dedupe against) an unrelated live request.
|