@buoy-gg/network 7.0.16 → 7.0.18
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/lib/commonjs/network/components/NetworkEventDetailView.js +18 -12
- package/lib/commonjs/network/components/NetworkEventDetailView.web.js +18 -12
- package/lib/commonjs/network/sync/networkSyncAdapter.js +265 -26
- package/lib/module/network/components/NetworkEventDetailView.js +18 -12
- package/lib/module/network/components/NetworkEventDetailView.web.js +18 -12
- package/lib/module/network/sync/networkSyncAdapter.js +265 -25
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.web.d.ts.map +1 -1
- package/lib/typescript/network/sync/__tests__/networkSyncAdapter.test.d.ts +2 -0
- package/lib/typescript/network/sync/__tests__/networkSyncAdapter.test.d.ts.map +1 -0
- package/lib/typescript/network/sync/networkBodyResolver.d.ts +2 -0
- package/lib/typescript/network/sync/networkBodyResolver.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +55 -19
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +14 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -324,13 +324,17 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
324
324
|
// fetched on demand through the resolver. On the device there is no resolver
|
|
325
325
|
// and bodies are always inline, so this is a no-op there.
|
|
326
326
|
const resolveBody = (0, _networkBodyResolver.useNetworkBodyResolver)();
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// The explicit flag is authoritative (adapter v5+). The size>0 fallback keeps
|
|
328
|
+
// detail-on-demand working against a device on an older adapter, which marked
|
|
329
|
+
// a withheld body only by leaving `data` undefined with a non-zero size.
|
|
330
|
+
const requestBodyOmitted = event.requestData === undefined && (event.requestBodyOmitted === true || (event.requestSize ?? 0) > 0);
|
|
331
|
+
const responseBodyOmitted = event.responseData === undefined && (event.responseBodyOmitted === true || (event.responseSize ?? 0) > 0);
|
|
332
|
+
const headersOmitted = event.headersOmitted === true;
|
|
329
333
|
const [resolvedBody, setResolvedBody] = (0, _react.useState)(null);
|
|
330
334
|
const [isLoadingBody, setIsLoadingBody] = (0, _react.useState)(false);
|
|
331
335
|
(0, _react.useEffect)(() => {
|
|
332
336
|
setResolvedBody(null);
|
|
333
|
-
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted) {
|
|
337
|
+
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted && !headersOmitted) {
|
|
334
338
|
setIsLoadingBody(false);
|
|
335
339
|
return;
|
|
336
340
|
}
|
|
@@ -344,12 +348,14 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
344
348
|
return () => {
|
|
345
349
|
cancelled = true;
|
|
346
350
|
};
|
|
347
|
-
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted]);
|
|
351
|
+
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted, headersOmitted]);
|
|
348
352
|
|
|
349
353
|
// Effective bodies: inline data when present, otherwise the lazily resolved
|
|
350
354
|
// body. `null`/`undefined` both collapse to undefined ("no body").
|
|
351
355
|
const requestData = event.requestData ?? resolvedBody?.requestData ?? undefined;
|
|
352
356
|
const responseData = event.responseData ?? resolvedBody?.responseData ?? undefined;
|
|
357
|
+
const requestHeaders = (headersOmitted ? resolvedBody?.requestHeaders : undefined) ?? event.requestHeaders;
|
|
358
|
+
const responseHeaders = (headersOmitted ? resolvedBody?.responseHeaders : undefined) ?? event.responseHeaders;
|
|
353
359
|
|
|
354
360
|
// Generate full request details for copying
|
|
355
361
|
const getFullRequestDetails = () => {
|
|
@@ -360,9 +366,9 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
360
366
|
statusText: event.statusText,
|
|
361
367
|
timestamp: new Date(event.timestamp).toISOString(),
|
|
362
368
|
duration: event.duration ? `${event.duration}ms` : "N/A",
|
|
363
|
-
requestHeaders
|
|
369
|
+
requestHeaders,
|
|
364
370
|
requestData,
|
|
365
|
-
responseHeaders
|
|
371
|
+
responseHeaders,
|
|
366
372
|
responseData,
|
|
367
373
|
requestSize: event.requestSize ? (0, _formatting.formatBytes)(event.requestSize) : "N/A",
|
|
368
374
|
responseSize: event.responseSize ? (0, _formatting.formatBytes)(event.responseSize) : "N/A",
|
|
@@ -551,12 +557,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
551
557
|
color: _sharedUi.macOSColors.semantic.info
|
|
552
558
|
}),
|
|
553
559
|
defaultOpen: false,
|
|
554
|
-
count: Object.keys(
|
|
555
|
-
children: Object.keys(
|
|
560
|
+
count: Object.keys(requestHeaders).length,
|
|
561
|
+
children: Object.keys(requestHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
556
562
|
style: styles.dataViewerContainer,
|
|
557
563
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
558
564
|
title: "",
|
|
559
|
-
data:
|
|
565
|
+
data: requestHeaders,
|
|
560
566
|
showTypeFilter: true,
|
|
561
567
|
rawMode: true,
|
|
562
568
|
initialExpanded: true,
|
|
@@ -573,12 +579,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
573
579
|
color: NEUTRAL_ICON
|
|
574
580
|
}),
|
|
575
581
|
defaultOpen: false,
|
|
576
|
-
count: Object.keys(
|
|
577
|
-
children: Object.keys(
|
|
582
|
+
count: Object.keys(responseHeaders).length,
|
|
583
|
+
children: Object.keys(responseHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
578
584
|
style: styles.dataViewerContainer,
|
|
579
585
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
|
|
580
586
|
title: "",
|
|
581
|
-
data:
|
|
587
|
+
data: responseHeaders,
|
|
582
588
|
showTypeFilter: true,
|
|
583
589
|
rawMode: true,
|
|
584
590
|
initialExpanded: true,
|
|
@@ -637,13 +637,17 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
637
637
|
// webview port there is normally no resolver and bodies are always inline,
|
|
638
638
|
// so this is a no-op there — exactly like on the device.
|
|
639
639
|
const resolveBody = (0, _networkBodyResolver.useNetworkBodyResolver)();
|
|
640
|
-
|
|
641
|
-
|
|
640
|
+
// The explicit flag is authoritative (adapter v5+). The size>0 fallback keeps
|
|
641
|
+
// detail-on-demand working against a device on an older adapter, which marked
|
|
642
|
+
// a withheld body only by leaving `data` undefined with a non-zero size.
|
|
643
|
+
const requestBodyOmitted = event.requestData === undefined && (event.requestBodyOmitted === true || (event.requestSize ?? 0) > 0);
|
|
644
|
+
const responseBodyOmitted = event.responseData === undefined && (event.responseBodyOmitted === true || (event.responseSize ?? 0) > 0);
|
|
645
|
+
const headersOmitted = event.headersOmitted === true;
|
|
642
646
|
const [resolvedBody, setResolvedBody] = (0, _react.useState)(null);
|
|
643
647
|
const [isLoadingBody, setIsLoadingBody] = (0, _react.useState)(false);
|
|
644
648
|
(0, _react.useEffect)(() => {
|
|
645
649
|
setResolvedBody(null);
|
|
646
|
-
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted) {
|
|
650
|
+
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted && !headersOmitted) {
|
|
647
651
|
setIsLoadingBody(false);
|
|
648
652
|
return;
|
|
649
653
|
}
|
|
@@ -657,12 +661,14 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
657
661
|
return () => {
|
|
658
662
|
cancelled = true;
|
|
659
663
|
};
|
|
660
|
-
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted]);
|
|
664
|
+
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted, headersOmitted]);
|
|
661
665
|
|
|
662
666
|
// Effective bodies: inline data when present, otherwise the lazily resolved
|
|
663
667
|
// body. `null`/`undefined` both collapse to undefined ("no body").
|
|
664
668
|
const requestData = event.requestData ?? resolvedBody?.requestData ?? undefined;
|
|
665
669
|
const responseData = event.responseData ?? resolvedBody?.responseData ?? undefined;
|
|
670
|
+
const requestHeaders = (headersOmitted ? resolvedBody?.requestHeaders : undefined) ?? event.requestHeaders;
|
|
671
|
+
const responseHeaders = (headersOmitted ? resolvedBody?.responseHeaders : undefined) ?? event.responseHeaders;
|
|
666
672
|
|
|
667
673
|
// Generate full request details for copying (resolved LAZILY at press time —
|
|
668
674
|
// see the copy button below).
|
|
@@ -674,9 +680,9 @@ const NetworkEventDetailView = exports.NetworkEventDetailView = /*#__PURE__*/(0,
|
|
|
674
680
|
statusText: event.statusText,
|
|
675
681
|
timestamp: new Date(event.timestamp).toISOString(),
|
|
676
682
|
duration: event.duration ? `${event.duration}ms` : "N/A",
|
|
677
|
-
requestHeaders
|
|
683
|
+
requestHeaders,
|
|
678
684
|
requestData,
|
|
679
|
-
responseHeaders
|
|
685
|
+
responseHeaders,
|
|
680
686
|
responseData,
|
|
681
687
|
requestSize: event.requestSize ? formatBytes(event.requestSize) : "N/A",
|
|
682
688
|
responseSize: event.responseSize ? formatBytes(event.responseSize) : "N/A",
|
|
@@ -849,12 +855,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
849
855
|
color: macOSColors.semantic.info
|
|
850
856
|
}),
|
|
851
857
|
defaultOpen: false,
|
|
852
|
-
count: Object.keys(
|
|
853
|
-
children: Object.keys(
|
|
858
|
+
count: Object.keys(requestHeaders).length,
|
|
859
|
+
children: Object.keys(requestHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
854
860
|
style: styles.dataViewerContainer,
|
|
855
861
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DataViewer.DataViewer, {
|
|
856
862
|
title: "",
|
|
857
|
-
data:
|
|
863
|
+
data: requestHeaders,
|
|
858
864
|
showTypeFilter: true,
|
|
859
865
|
rawMode: true,
|
|
860
866
|
initialExpanded: true
|
|
@@ -870,12 +876,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
870
876
|
color: NEUTRAL_ICON
|
|
871
877
|
}),
|
|
872
878
|
defaultOpen: false,
|
|
873
|
-
count: Object.keys(
|
|
874
|
-
children: Object.keys(
|
|
879
|
+
count: Object.keys(responseHeaders).length,
|
|
880
|
+
children: Object.keys(responseHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
875
881
|
style: styles.dataViewerContainer,
|
|
876
882
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DataViewer.DataViewer, {
|
|
877
883
|
title: "",
|
|
878
|
-
data:
|
|
884
|
+
data: responseHeaders,
|
|
879
885
|
showTypeFilter: true,
|
|
880
886
|
rawMode: true,
|
|
881
887
|
initialExpanded: true
|
|
@@ -3,38 +3,232 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.networkSyncAdapter = exports.SNAPSHOT_BODY_INLINE_LIMIT = void 0;
|
|
6
|
+
exports.networkSyncAdapter = exports.SNAPSHOT_HEADER_VALUE_LIMIT = exports.SNAPSHOT_EVENTS_BUDGET = exports.SNAPSHOT_BODY_INLINE_LIMIT = void 0;
|
|
7
7
|
var _networkEventStore = require("../utils/networkEventStore");
|
|
8
8
|
var _networkSavedStore = require("../utils/networkSavedStore");
|
|
9
9
|
var _overrideRulesStore = require("../overrides/overrideRulesStore");
|
|
10
10
|
var _presets = require("../overrides/presets");
|
|
11
11
|
var _engine = require("../overrides/engine");
|
|
12
12
|
var _networkListener = require("../utils/networkListener");
|
|
13
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
13
14
|
/**
|
|
14
|
-
* Request/response bodies larger than this
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* dashboard's main thread while it deserializes and re-renders them. Smaller
|
|
19
|
-
* bodies stay inline so the common case needs no round-trip (instant detail
|
|
20
|
-
* view, and they remain available to copy/export). Stripped bodies are fetched
|
|
21
|
-
* on demand via the `getEventBody` action when a request's detail view opens.
|
|
15
|
+
* Request/response bodies larger than this are NOT streamed in the list
|
|
16
|
+
* snapshot. Prefer the cached size proxy already on the event (free); if that
|
|
17
|
+
* field is missing or wrong (0 for a fat/cyclic body), fall back to an
|
|
18
|
+
* O(limit) walk. Stripped bodies come back via `getEventBody` on detail open.
|
|
22
19
|
*/
|
|
23
20
|
const SNAPSHOT_BODY_INLINE_LIMIT = exports.SNAPSHOT_BODY_INLINE_LIMIT = 16 * 1024;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Total inline budget for one snapshot's event list, held under the emit
|
|
24
|
+
* layer's 2MB so a snapshot is never DROPPED (a drop leaves the whole panel
|
|
25
|
+
* stale). Per-field caps alone don't get there: 500 requests with 9KB
|
|
26
|
+
* responses are all individually legal and still add up to ~4.7MB. Newest
|
|
27
|
+
* events keep their bodies inline (that's what a developer is looking at);
|
|
28
|
+
* once the budget is spent, older events degrade to metadata + fetch-on-demand.
|
|
29
|
+
*/
|
|
30
|
+
const SNAPSHOT_EVENTS_BUDGET = exports.SNAPSHOT_EVENTS_BUDGET = 1.25 * 1024 * 1024;
|
|
31
|
+
function shouldStripBody(data, reportedSize) {
|
|
32
|
+
if (data === undefined) return false;
|
|
33
|
+
// The cached size proxy already on the event is free when it's right; the
|
|
34
|
+
// walk is the fallback for when it's missing or wrong (0 for a fat body).
|
|
35
|
+
if ((reportedSize ?? 0) > SNAPSHOT_BODY_INLINE_LIMIT) return true;
|
|
36
|
+
return (0, _sharedUi.isOverWireBudget)(data, SNAPSHOT_BODY_INLINE_LIMIT);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One event's wire form plus its measured cost, so the budget pass is free. */
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Per-event wire cache. `networkEventStore` never mutates an event in place —
|
|
43
|
+
* a response/override update replaces the object — so keying on identity is
|
|
44
|
+
* correct and self-invalidating, and a completed request is converted once
|
|
45
|
+
* instead of on all ~5 snapshots per second for as long as it stays in the
|
|
46
|
+
* list. This is what keeps the guards off the hot path.
|
|
47
|
+
*/
|
|
48
|
+
const wireCache = new WeakMap();
|
|
49
|
+
const strippedCache = new WeakMap();
|
|
50
|
+
|
|
51
|
+
/** Per-field guards: oversized bodies out, heavy headers/variables slimmed. */
|
|
52
|
+
function toWireEvent(event) {
|
|
53
|
+
const cached = wireCache.get(event);
|
|
54
|
+
if (cached) return cached;
|
|
55
|
+
let entry;
|
|
56
|
+
try {
|
|
57
|
+
entry = buildWireEvent(event);
|
|
58
|
+
} catch {
|
|
59
|
+
// A body with a throwing getter or `toJSON` can blow up the size walk
|
|
60
|
+
// itself. Degrading this ONE event to metadata beats letting getSnapshot
|
|
61
|
+
// throw, which would take the whole panel down with it.
|
|
62
|
+
entry = buildMetadataOnly(event);
|
|
63
|
+
}
|
|
64
|
+
wireCache.set(event, entry);
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
function buildMetadataOnly(event) {
|
|
31
68
|
const stripped = {
|
|
32
|
-
...event
|
|
69
|
+
...event,
|
|
70
|
+
requestData: undefined,
|
|
71
|
+
responseData: undefined,
|
|
72
|
+
graphqlVariables: undefined,
|
|
73
|
+
requestBodyOmitted: event.requestData !== undefined ? true : undefined,
|
|
74
|
+
responseBodyOmitted: event.responseData !== undefined ? true : undefined
|
|
33
75
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
76
|
+
// The payload fields are gone, so measuring this is safe even when measuring
|
|
77
|
+
// the original blew up. Fall back to a nominal cost only if even THIS throws
|
|
78
|
+
// (a hostile getter on the event itself rather than on its body).
|
|
79
|
+
let bytes = 1024;
|
|
80
|
+
try {
|
|
81
|
+
bytes = (0, _sharedUi.approxJsonSize)(stripped, SNAPSHOT_EVENTS_BUDGET).bytes;
|
|
82
|
+
} catch {
|
|
83
|
+
/* keep the nominal estimate */
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
event: stripped,
|
|
87
|
+
bytes
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildWireEvent(event) {
|
|
91
|
+
const stripRequest = shouldStripBody(event.requestData, event.requestSize);
|
|
92
|
+
const stripResponse = shouldStripBody(event.responseData, event.responseSize);
|
|
93
|
+
const graphqlVariables = toWireGraphqlVariables(event.graphqlVariables);
|
|
94
|
+
const requestHeaders = toWireHeaders(event.requestHeaders);
|
|
95
|
+
const responseHeaders = toWireHeaders(event.responseHeaders);
|
|
96
|
+
const changed = stripRequest || stripResponse || graphqlVariables !== event.graphqlVariables || requestHeaders !== event.requestHeaders || responseHeaders !== event.responseHeaders;
|
|
97
|
+
let wire = event;
|
|
98
|
+
if (changed) {
|
|
99
|
+
// `undefined` keys are omitted by JSON serialization, so dropping the data
|
|
100
|
+
// shrinks the wire payload. Sizes stay HONEST — the explicit
|
|
101
|
+
// request/responseBodyOmitted flags are what tell the dashboard to fetch,
|
|
102
|
+
// matching `OverrideRule.bodyOmitted`. Full values come from `getEventBody`.
|
|
103
|
+
wire = {
|
|
104
|
+
...event
|
|
105
|
+
};
|
|
106
|
+
if (stripRequest) {
|
|
107
|
+
wire.requestData = undefined;
|
|
108
|
+
wire.requestBodyOmitted = true;
|
|
109
|
+
}
|
|
110
|
+
if (stripResponse) {
|
|
111
|
+
wire.responseData = undefined;
|
|
112
|
+
wire.responseBodyOmitted = true;
|
|
113
|
+
}
|
|
114
|
+
if (graphqlVariables !== event.graphqlVariables) {
|
|
115
|
+
wire.graphqlVariables = graphqlVariables;
|
|
116
|
+
}
|
|
117
|
+
if (requestHeaders !== event.requestHeaders) {
|
|
118
|
+
wire.requestHeaders = requestHeaders;
|
|
119
|
+
wire.headersOmitted = true;
|
|
120
|
+
}
|
|
121
|
+
if (responseHeaders !== event.responseHeaders) {
|
|
122
|
+
wire.responseHeaders = responseHeaders;
|
|
123
|
+
wire.headersOmitted = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
event: wire,
|
|
128
|
+
bytes: (0, _sharedUi.approxJsonSize)(wire, SNAPSHOT_EVENTS_BUDGET).bytes
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Metadata-only form, used for the tail of the list once the snapshot budget
|
|
134
|
+
* is spent. Method/url/status/timing all stay — only the payloads go.
|
|
135
|
+
*/
|
|
136
|
+
function toStrippedEvent(event) {
|
|
137
|
+
const cached = strippedCache.get(event);
|
|
138
|
+
if (cached) return cached;
|
|
139
|
+
const {
|
|
140
|
+
event: wire
|
|
141
|
+
} = toWireEvent(event);
|
|
142
|
+
const stripped = {
|
|
143
|
+
...wire
|
|
144
|
+
};
|
|
145
|
+
if (stripped.requestData !== undefined) {
|
|
146
|
+
stripped.requestData = undefined;
|
|
147
|
+
stripped.requestBodyOmitted = true;
|
|
148
|
+
}
|
|
149
|
+
if (stripped.responseData !== undefined) {
|
|
150
|
+
stripped.responseData = undefined;
|
|
151
|
+
stripped.responseBodyOmitted = true;
|
|
152
|
+
}
|
|
153
|
+
if (stripped.graphqlVariables !== undefined) {
|
|
154
|
+
stripped.graphqlVariables = toWireGraphqlVariables(stripped.graphqlVariables, /* force */true);
|
|
155
|
+
}
|
|
156
|
+
const entry = {
|
|
157
|
+
event: stripped,
|
|
158
|
+
bytes: (0, _sharedUi.approxJsonSize)(stripped, SNAPSHOT_EVENTS_BUDGET).bytes
|
|
159
|
+
};
|
|
160
|
+
strippedCache.set(event, entry);
|
|
161
|
+
return entry;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Spend the snapshot budget newest-first. The store keeps events newest-first
|
|
166
|
+
* already, so this hands the developer full detail on what they are actually
|
|
167
|
+
* looking at and degrades the tail rather than dropping the whole panel.
|
|
168
|
+
*/
|
|
169
|
+
function fitEventsToBudget(events) {
|
|
170
|
+
const out = new Array(events.length);
|
|
171
|
+
let spent = 0;
|
|
172
|
+
for (let i = 0; i < events.length; i++) {
|
|
173
|
+
const full = toWireEvent(events[i]);
|
|
174
|
+
if (spent + full.bytes <= SNAPSHOT_EVENTS_BUDGET) {
|
|
175
|
+
out[i] = full.event;
|
|
176
|
+
spent += full.bytes;
|
|
177
|
+
} else {
|
|
178
|
+
const lean = toStrippedEvent(events[i]);
|
|
179
|
+
out[i] = lean.event;
|
|
180
|
+
spent += lean.bytes;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* GraphQL variables are copied off `request.data.variables` onto their own
|
|
188
|
+
* field for the list subtitle (`GetPokemon › Sandshrew`). Capping requestData
|
|
189
|
+
* still left that copy on the wire. Keep short primitives for the subtitle;
|
|
190
|
+
* drop nested/fat values (file uploads, base64). Full variables stay in
|
|
191
|
+
* `requestData` on-device and come back via `getEventBody`.
|
|
192
|
+
*/
|
|
193
|
+
function toWireGraphqlVariables(vars, force = false) {
|
|
194
|
+
if (!vars) return vars;
|
|
195
|
+
if (!force && !shouldStripBody(vars, undefined)) return vars;
|
|
196
|
+
const slim = {};
|
|
197
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
198
|
+
if (typeof value === "string" && value.length <= 64) slim[key] = value;else if (typeof value === "number" || typeof value === "boolean") {
|
|
199
|
+
slim[key] = value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(slim).length > 0 ? slim : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Header values (Cookie, Authorization, custom JSON) still rode every
|
|
207
|
+
* snapshot after bodies were stripped. 500 events × a 4KB session cookie
|
|
208
|
+
* is a 2MB drop of the whole Network panel. Keep short values inline
|
|
209
|
+
* (content-type, accept); truncate the rest. Full maps stay on-device
|
|
210
|
+
* and come back via `getEventBody`.
|
|
211
|
+
*
|
|
212
|
+
* Returns the SAME object when nothing was truncated: callers use identity to
|
|
213
|
+
* decide whether anything was omitted, so a fresh `{}` here would flag every
|
|
214
|
+
* header-less event as truncated (and defeat the no-copy fast path).
|
|
215
|
+
*/
|
|
216
|
+
const SNAPSHOT_HEADER_VALUE_LIMIT = exports.SNAPSHOT_HEADER_VALUE_LIMIT = 64;
|
|
217
|
+
function toWireHeaders(headers) {
|
|
218
|
+
if (!headers) return headers;
|
|
219
|
+
let changed = false;
|
|
220
|
+
const slim = {};
|
|
221
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
222
|
+
if (typeof value !== "string") continue;
|
|
223
|
+
if (value.length <= SNAPSHOT_HEADER_VALUE_LIMIT) {
|
|
224
|
+
slim[key] = value;
|
|
225
|
+
} else {
|
|
226
|
+
slim[key] = `${value.slice(0, SNAPSHOT_HEADER_VALUE_LIMIT)}… [${value.length - SNAPSHOT_HEADER_VALUE_LIMIT} more]`;
|
|
227
|
+
changed = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return changed ? slim : headers;
|
|
231
|
+
}
|
|
38
232
|
|
|
39
233
|
/**
|
|
40
234
|
* Drop oversized rule bodies from the snapshot, flagging what was dropped.
|
|
@@ -152,7 +346,9 @@ function baseFromRequest(params) {
|
|
|
152
346
|
* this package doesn't need a dependency on it).
|
|
153
347
|
*
|
|
154
348
|
* Subscribing starts the network interceptor, so requests are only captured
|
|
155
|
-
* while a dashboard is watching the network panel
|
|
349
|
+
* while a dashboard is watching the network panel — or while an MCP
|
|
350
|
+
* client holds a capture-mode watch (`devtool-watch` with `stream:false`),
|
|
351
|
+
* which records without asking the device to push snapshots.
|
|
156
352
|
*
|
|
157
353
|
* v2: large bodies are stripped from the snapshot and fetched lazily via
|
|
158
354
|
* `getEventBody` (see SNAPSHOT_BODY_INLINE_LIMIT).
|
|
@@ -164,9 +360,12 @@ function baseFromRequest(params) {
|
|
|
164
360
|
* `NetworkPanel`/`NetworkInline` accept either shape.
|
|
165
361
|
*/
|
|
166
362
|
const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
167
|
-
|
|
363
|
+
// v5: honest request/responseSize + explicit request/responseBodyOmitted
|
|
364
|
+
// flags, and a total snapshot budget so the list degrades instead of the
|
|
365
|
+
// whole panel being dropped at the emit layer.
|
|
366
|
+
version: 5,
|
|
168
367
|
getSnapshot: () => ({
|
|
169
|
-
events: _networkEventStore.networkEventStore.getEvents()
|
|
368
|
+
events: fitEventsToBudget(_networkEventStore.networkEventStore.getEvents()),
|
|
170
369
|
/**
|
|
171
370
|
* Response-override rules live on the DEVICE — that's the only place they
|
|
172
371
|
* can run, since a rule is applied inside the app's own XMLHttpRequest.
|
|
@@ -178,7 +377,7 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
178
377
|
// the same 200ms loop that the body-stripping exists to protect.
|
|
179
378
|
saved: _networkSavedStore.networkSavedStore.getRecords().map(record => ({
|
|
180
379
|
...record,
|
|
181
|
-
event:
|
|
380
|
+
event: toWireEvent(record.event).event
|
|
182
381
|
}))
|
|
183
382
|
}),
|
|
184
383
|
subscribe: onChange => {
|
|
@@ -212,7 +411,9 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
212
411
|
if (!event) return null;
|
|
213
412
|
return {
|
|
214
413
|
requestData: event.requestData ?? null,
|
|
215
|
-
responseData: event.responseData ?? null
|
|
414
|
+
responseData: event.responseData ?? null,
|
|
415
|
+
requestHeaders: event.requestHeaders ?? null,
|
|
416
|
+
responseHeaders: event.responseHeaders ?? null
|
|
216
417
|
};
|
|
217
418
|
},
|
|
218
419
|
/** Pin/unpin a request from the dashboard. */
|
|
@@ -359,6 +560,44 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
359
560
|
};
|
|
360
561
|
},
|
|
361
562
|
/** Why isn't my rule firing? Reports the engine's own view of the world. */
|
|
563
|
+
/**
|
|
564
|
+
* Is the device actually recording right now, and why not?
|
|
565
|
+
*
|
|
566
|
+
* The honest answer to an empty request list. Without this a caller can
|
|
567
|
+
* only guess, and the plausible guesses ("interception is broken on this
|
|
568
|
+
* runtime") are all wrong — the usual answer is simply that nothing is
|
|
569
|
+
* holding capture open.
|
|
570
|
+
*/
|
|
571
|
+
getCaptureStatus: () => {
|
|
572
|
+
// RECORDING is what the caller actually wants to know, and it is driven
|
|
573
|
+
// by the store's subscriber count — not by whether the interceptor is
|
|
574
|
+
// installed. Those genuinely differ: an enabled override rule pins the
|
|
575
|
+
// interceptor (`overrideRulesStore.syncKeepAlive`) without anything
|
|
576
|
+
// writing to the event store, so reporting `isNetworkListening()` as
|
|
577
|
+
// "capturing" would answer "yes" to a device recording nothing. That is
|
|
578
|
+
// the same species of misleading answer this action exists to replace.
|
|
579
|
+
const subscribers = _networkEventStore.networkEventStore.getSubscriberCounts().total;
|
|
580
|
+
|
|
581
|
+
// `isNetworkListening()` lazily CONSTRUCTS the listener, which reaches
|
|
582
|
+
// for XMLHttpRequest — absent in Node and in any headless host. A
|
|
583
|
+
// diagnostic that throws is worse than no diagnostic, so probe
|
|
584
|
+
// defensively and report what we do know either way.
|
|
585
|
+
let interceptorInstalled = null;
|
|
586
|
+
let listenerCount = null;
|
|
587
|
+
try {
|
|
588
|
+
interceptorInstalled = (0, _networkListener.isNetworkListening)();
|
|
589
|
+
listenerCount = (0, _networkListener.getNetworkListenerCount)();
|
|
590
|
+
} catch {
|
|
591
|
+
/* no interceptable runtime here */
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
capturing: subscribers > 0,
|
|
595
|
+
subscribers,
|
|
596
|
+
interceptorInstalled,
|
|
597
|
+
listenerCount,
|
|
598
|
+
eventCount: _networkEventStore.networkEventStore.getEvents().length
|
|
599
|
+
};
|
|
600
|
+
},
|
|
362
601
|
debugOverrides: params => {
|
|
363
602
|
const url = (params && typeof params === "object" ? params.url : undefined) ?? "https://example.com/";
|
|
364
603
|
return {
|
|
@@ -323,13 +323,17 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
323
323
|
// fetched on demand through the resolver. On the device there is no resolver
|
|
324
324
|
// and bodies are always inline, so this is a no-op there.
|
|
325
325
|
const resolveBody = useNetworkBodyResolver();
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
// The explicit flag is authoritative (adapter v5+). The size>0 fallback keeps
|
|
327
|
+
// detail-on-demand working against a device on an older adapter, which marked
|
|
328
|
+
// a withheld body only by leaving `data` undefined with a non-zero size.
|
|
329
|
+
const requestBodyOmitted = event.requestData === undefined && (event.requestBodyOmitted === true || (event.requestSize ?? 0) > 0);
|
|
330
|
+
const responseBodyOmitted = event.responseData === undefined && (event.responseBodyOmitted === true || (event.responseSize ?? 0) > 0);
|
|
331
|
+
const headersOmitted = event.headersOmitted === true;
|
|
328
332
|
const [resolvedBody, setResolvedBody] = useState(null);
|
|
329
333
|
const [isLoadingBody, setIsLoadingBody] = useState(false);
|
|
330
334
|
useEffect(() => {
|
|
331
335
|
setResolvedBody(null);
|
|
332
|
-
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted) {
|
|
336
|
+
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted && !headersOmitted) {
|
|
333
337
|
setIsLoadingBody(false);
|
|
334
338
|
return;
|
|
335
339
|
}
|
|
@@ -343,12 +347,14 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
343
347
|
return () => {
|
|
344
348
|
cancelled = true;
|
|
345
349
|
};
|
|
346
|
-
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted]);
|
|
350
|
+
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted, headersOmitted]);
|
|
347
351
|
|
|
348
352
|
// Effective bodies: inline data when present, otherwise the lazily resolved
|
|
349
353
|
// body. `null`/`undefined` both collapse to undefined ("no body").
|
|
350
354
|
const requestData = event.requestData ?? resolvedBody?.requestData ?? undefined;
|
|
351
355
|
const responseData = event.responseData ?? resolvedBody?.responseData ?? undefined;
|
|
356
|
+
const requestHeaders = (headersOmitted ? resolvedBody?.requestHeaders : undefined) ?? event.requestHeaders;
|
|
357
|
+
const responseHeaders = (headersOmitted ? resolvedBody?.responseHeaders : undefined) ?? event.responseHeaders;
|
|
352
358
|
|
|
353
359
|
// Generate full request details for copying
|
|
354
360
|
const getFullRequestDetails = () => {
|
|
@@ -359,9 +365,9 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
359
365
|
statusText: event.statusText,
|
|
360
366
|
timestamp: new Date(event.timestamp).toISOString(),
|
|
361
367
|
duration: event.duration ? `${event.duration}ms` : "N/A",
|
|
362
|
-
requestHeaders
|
|
368
|
+
requestHeaders,
|
|
363
369
|
requestData,
|
|
364
|
-
responseHeaders
|
|
370
|
+
responseHeaders,
|
|
365
371
|
responseData,
|
|
366
372
|
requestSize: event.requestSize ? formatBytes(event.requestSize) : "N/A",
|
|
367
373
|
responseSize: event.responseSize ? formatBytes(event.responseSize) : "N/A",
|
|
@@ -550,12 +556,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
550
556
|
color: macOSColors.semantic.info
|
|
551
557
|
}),
|
|
552
558
|
defaultOpen: false,
|
|
553
|
-
count: Object.keys(
|
|
554
|
-
children: Object.keys(
|
|
559
|
+
count: Object.keys(requestHeaders).length,
|
|
560
|
+
children: Object.keys(requestHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
|
|
555
561
|
style: styles.dataViewerContainer,
|
|
556
562
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
557
563
|
title: "",
|
|
558
|
-
data:
|
|
564
|
+
data: requestHeaders,
|
|
559
565
|
showTypeFilter: true,
|
|
560
566
|
rawMode: true,
|
|
561
567
|
initialExpanded: true,
|
|
@@ -572,12 +578,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
572
578
|
color: NEUTRAL_ICON
|
|
573
579
|
}),
|
|
574
580
|
defaultOpen: false,
|
|
575
|
-
count: Object.keys(
|
|
576
|
-
children: Object.keys(
|
|
581
|
+
count: Object.keys(responseHeaders).length,
|
|
582
|
+
children: Object.keys(responseHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
|
|
577
583
|
style: styles.dataViewerContainer,
|
|
578
584
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
579
585
|
title: "",
|
|
580
|
-
data:
|
|
586
|
+
data: responseHeaders,
|
|
581
587
|
showTypeFilter: true,
|
|
582
588
|
rawMode: true,
|
|
583
589
|
initialExpanded: true,
|
|
@@ -635,13 +635,17 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
635
635
|
// webview port there is normally no resolver and bodies are always inline,
|
|
636
636
|
// so this is a no-op there — exactly like on the device.
|
|
637
637
|
const resolveBody = useNetworkBodyResolver();
|
|
638
|
-
|
|
639
|
-
|
|
638
|
+
// The explicit flag is authoritative (adapter v5+). The size>0 fallback keeps
|
|
639
|
+
// detail-on-demand working against a device on an older adapter, which marked
|
|
640
|
+
// a withheld body only by leaving `data` undefined with a non-zero size.
|
|
641
|
+
const requestBodyOmitted = event.requestData === undefined && (event.requestBodyOmitted === true || (event.requestSize ?? 0) > 0);
|
|
642
|
+
const responseBodyOmitted = event.responseData === undefined && (event.responseBodyOmitted === true || (event.responseSize ?? 0) > 0);
|
|
643
|
+
const headersOmitted = event.headersOmitted === true;
|
|
640
644
|
const [resolvedBody, setResolvedBody] = useState(null);
|
|
641
645
|
const [isLoadingBody, setIsLoadingBody] = useState(false);
|
|
642
646
|
useEffect(() => {
|
|
643
647
|
setResolvedBody(null);
|
|
644
|
-
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted) {
|
|
648
|
+
if (!resolveBody || !requestBodyOmitted && !responseBodyOmitted && !headersOmitted) {
|
|
645
649
|
setIsLoadingBody(false);
|
|
646
650
|
return;
|
|
647
651
|
}
|
|
@@ -655,12 +659,14 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
655
659
|
return () => {
|
|
656
660
|
cancelled = true;
|
|
657
661
|
};
|
|
658
|
-
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted]);
|
|
662
|
+
}, [event.id, resolveBody, requestBodyOmitted, responseBodyOmitted, headersOmitted]);
|
|
659
663
|
|
|
660
664
|
// Effective bodies: inline data when present, otherwise the lazily resolved
|
|
661
665
|
// body. `null`/`undefined` both collapse to undefined ("no body").
|
|
662
666
|
const requestData = event.requestData ?? resolvedBody?.requestData ?? undefined;
|
|
663
667
|
const responseData = event.responseData ?? resolvedBody?.responseData ?? undefined;
|
|
668
|
+
const requestHeaders = (headersOmitted ? resolvedBody?.requestHeaders : undefined) ?? event.requestHeaders;
|
|
669
|
+
const responseHeaders = (headersOmitted ? resolvedBody?.responseHeaders : undefined) ?? event.responseHeaders;
|
|
664
670
|
|
|
665
671
|
// Generate full request details for copying (resolved LAZILY at press time —
|
|
666
672
|
// see the copy button below).
|
|
@@ -672,9 +678,9 @@ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDet
|
|
|
672
678
|
statusText: event.statusText,
|
|
673
679
|
timestamp: new Date(event.timestamp).toISOString(),
|
|
674
680
|
duration: event.duration ? `${event.duration}ms` : "N/A",
|
|
675
|
-
requestHeaders
|
|
681
|
+
requestHeaders,
|
|
676
682
|
requestData,
|
|
677
|
-
responseHeaders
|
|
683
|
+
responseHeaders,
|
|
678
684
|
responseData,
|
|
679
685
|
requestSize: event.requestSize ? formatBytes(event.requestSize) : "N/A",
|
|
680
686
|
responseSize: event.responseSize ? formatBytes(event.responseSize) : "N/A",
|
|
@@ -847,12 +853,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
847
853
|
color: macOSColors.semantic.info
|
|
848
854
|
}),
|
|
849
855
|
defaultOpen: false,
|
|
850
|
-
count: Object.keys(
|
|
851
|
-
children: Object.keys(
|
|
856
|
+
count: Object.keys(requestHeaders).length,
|
|
857
|
+
children: Object.keys(requestHeaders).length > 0 ? /*#__PURE__*/_jsx("div", {
|
|
852
858
|
style: styles.dataViewerContainer,
|
|
853
859
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
854
860
|
title: "",
|
|
855
|
-
data:
|
|
861
|
+
data: requestHeaders,
|
|
856
862
|
showTypeFilter: true,
|
|
857
863
|
rawMode: true,
|
|
858
864
|
initialExpanded: true
|
|
@@ -868,12 +874,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
868
874
|
color: NEUTRAL_ICON
|
|
869
875
|
}),
|
|
870
876
|
defaultOpen: false,
|
|
871
|
-
count: Object.keys(
|
|
872
|
-
children: Object.keys(
|
|
877
|
+
count: Object.keys(responseHeaders).length,
|
|
878
|
+
children: Object.keys(responseHeaders).length > 0 ? /*#__PURE__*/_jsx("div", {
|
|
873
879
|
style: styles.dataViewerContainer,
|
|
874
880
|
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
875
881
|
title: "",
|
|
876
|
-
data:
|
|
882
|
+
data: responseHeaders,
|
|
877
883
|
showTypeFilter: true,
|
|
878
884
|
rawMode: true,
|
|
879
885
|
initialExpanded: true
|
|
@@ -6,31 +6,226 @@ import { overrideRulesStore } from "../overrides/overrideRulesStore";
|
|
|
6
6
|
import { draftFromEvent } from "../overrides/presets";
|
|
7
7
|
import { inspectEngine } from "../overrides/engine";
|
|
8
8
|
import { getNetworkListenerCount, isNetworkListening } from "../utils/networkListener";
|
|
9
|
+
import { approxJsonSize, isOverWireBudget } from "@buoy-gg/shared-ui";
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
|
-
* Request/response bodies larger than this
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* dashboard's main thread while it deserializes and re-renders them. Smaller
|
|
15
|
-
* bodies stay inline so the common case needs no round-trip (instant detail
|
|
16
|
-
* view, and they remain available to copy/export). Stripped bodies are fetched
|
|
17
|
-
* on demand via the `getEventBody` action when a request's detail view opens.
|
|
12
|
+
* Request/response bodies larger than this are NOT streamed in the list
|
|
13
|
+
* snapshot. Prefer the cached size proxy already on the event (free); if that
|
|
14
|
+
* field is missing or wrong (0 for a fat/cyclic body), fall back to an
|
|
15
|
+
* O(limit) walk. Stripped bodies come back via `getEventBody` on detail open.
|
|
18
16
|
*/
|
|
19
17
|
export const SNAPSHOT_BODY_INLINE_LIMIT = 16 * 1024;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Total inline budget for one snapshot's event list, held under the emit
|
|
21
|
+
* layer's 2MB so a snapshot is never DROPPED (a drop leaves the whole panel
|
|
22
|
+
* stale). Per-field caps alone don't get there: 500 requests with 9KB
|
|
23
|
+
* responses are all individually legal and still add up to ~4.7MB. Newest
|
|
24
|
+
* events keep their bodies inline (that's what a developer is looking at);
|
|
25
|
+
* once the budget is spent, older events degrade to metadata + fetch-on-demand.
|
|
26
|
+
*/
|
|
27
|
+
export const SNAPSHOT_EVENTS_BUDGET = 1.25 * 1024 * 1024;
|
|
28
|
+
function shouldStripBody(data, reportedSize) {
|
|
29
|
+
if (data === undefined) return false;
|
|
30
|
+
// The cached size proxy already on the event is free when it's right; the
|
|
31
|
+
// walk is the fallback for when it's missing or wrong (0 for a fat body).
|
|
32
|
+
if ((reportedSize ?? 0) > SNAPSHOT_BODY_INLINE_LIMIT) return true;
|
|
33
|
+
return isOverWireBudget(data, SNAPSHOT_BODY_INLINE_LIMIT);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One event's wire form plus its measured cost, so the budget pass is free. */
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-event wire cache. `networkEventStore` never mutates an event in place —
|
|
40
|
+
* a response/override update replaces the object — so keying on identity is
|
|
41
|
+
* correct and self-invalidating, and a completed request is converted once
|
|
42
|
+
* instead of on all ~5 snapshots per second for as long as it stays in the
|
|
43
|
+
* list. This is what keeps the guards off the hot path.
|
|
44
|
+
*/
|
|
45
|
+
const wireCache = new WeakMap();
|
|
46
|
+
const strippedCache = new WeakMap();
|
|
47
|
+
|
|
48
|
+
/** Per-field guards: oversized bodies out, heavy headers/variables slimmed. */
|
|
49
|
+
function toWireEvent(event) {
|
|
50
|
+
const cached = wireCache.get(event);
|
|
51
|
+
if (cached) return cached;
|
|
52
|
+
let entry;
|
|
53
|
+
try {
|
|
54
|
+
entry = buildWireEvent(event);
|
|
55
|
+
} catch {
|
|
56
|
+
// A body with a throwing getter or `toJSON` can blow up the size walk
|
|
57
|
+
// itself. Degrading this ONE event to metadata beats letting getSnapshot
|
|
58
|
+
// throw, which would take the whole panel down with it.
|
|
59
|
+
entry = buildMetadataOnly(event);
|
|
60
|
+
}
|
|
61
|
+
wireCache.set(event, entry);
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
function buildMetadataOnly(event) {
|
|
27
65
|
const stripped = {
|
|
28
|
-
...event
|
|
66
|
+
...event,
|
|
67
|
+
requestData: undefined,
|
|
68
|
+
responseData: undefined,
|
|
69
|
+
graphqlVariables: undefined,
|
|
70
|
+
requestBodyOmitted: event.requestData !== undefined ? true : undefined,
|
|
71
|
+
responseBodyOmitted: event.responseData !== undefined ? true : undefined
|
|
29
72
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
73
|
+
// The payload fields are gone, so measuring this is safe even when measuring
|
|
74
|
+
// the original blew up. Fall back to a nominal cost only if even THIS throws
|
|
75
|
+
// (a hostile getter on the event itself rather than on its body).
|
|
76
|
+
let bytes = 1024;
|
|
77
|
+
try {
|
|
78
|
+
bytes = approxJsonSize(stripped, SNAPSHOT_EVENTS_BUDGET).bytes;
|
|
79
|
+
} catch {
|
|
80
|
+
/* keep the nominal estimate */
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
event: stripped,
|
|
84
|
+
bytes
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildWireEvent(event) {
|
|
88
|
+
const stripRequest = shouldStripBody(event.requestData, event.requestSize);
|
|
89
|
+
const stripResponse = shouldStripBody(event.responseData, event.responseSize);
|
|
90
|
+
const graphqlVariables = toWireGraphqlVariables(event.graphqlVariables);
|
|
91
|
+
const requestHeaders = toWireHeaders(event.requestHeaders);
|
|
92
|
+
const responseHeaders = toWireHeaders(event.responseHeaders);
|
|
93
|
+
const changed = stripRequest || stripResponse || graphqlVariables !== event.graphqlVariables || requestHeaders !== event.requestHeaders || responseHeaders !== event.responseHeaders;
|
|
94
|
+
let wire = event;
|
|
95
|
+
if (changed) {
|
|
96
|
+
// `undefined` keys are omitted by JSON serialization, so dropping the data
|
|
97
|
+
// shrinks the wire payload. Sizes stay HONEST — the explicit
|
|
98
|
+
// request/responseBodyOmitted flags are what tell the dashboard to fetch,
|
|
99
|
+
// matching `OverrideRule.bodyOmitted`. Full values come from `getEventBody`.
|
|
100
|
+
wire = {
|
|
101
|
+
...event
|
|
102
|
+
};
|
|
103
|
+
if (stripRequest) {
|
|
104
|
+
wire.requestData = undefined;
|
|
105
|
+
wire.requestBodyOmitted = true;
|
|
106
|
+
}
|
|
107
|
+
if (stripResponse) {
|
|
108
|
+
wire.responseData = undefined;
|
|
109
|
+
wire.responseBodyOmitted = true;
|
|
110
|
+
}
|
|
111
|
+
if (graphqlVariables !== event.graphqlVariables) {
|
|
112
|
+
wire.graphqlVariables = graphqlVariables;
|
|
113
|
+
}
|
|
114
|
+
if (requestHeaders !== event.requestHeaders) {
|
|
115
|
+
wire.requestHeaders = requestHeaders;
|
|
116
|
+
wire.headersOmitted = true;
|
|
117
|
+
}
|
|
118
|
+
if (responseHeaders !== event.responseHeaders) {
|
|
119
|
+
wire.responseHeaders = responseHeaders;
|
|
120
|
+
wire.headersOmitted = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
event: wire,
|
|
125
|
+
bytes: approxJsonSize(wire, SNAPSHOT_EVENTS_BUDGET).bytes
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Metadata-only form, used for the tail of the list once the snapshot budget
|
|
131
|
+
* is spent. Method/url/status/timing all stay — only the payloads go.
|
|
132
|
+
*/
|
|
133
|
+
function toStrippedEvent(event) {
|
|
134
|
+
const cached = strippedCache.get(event);
|
|
135
|
+
if (cached) return cached;
|
|
136
|
+
const {
|
|
137
|
+
event: wire
|
|
138
|
+
} = toWireEvent(event);
|
|
139
|
+
const stripped = {
|
|
140
|
+
...wire
|
|
141
|
+
};
|
|
142
|
+
if (stripped.requestData !== undefined) {
|
|
143
|
+
stripped.requestData = undefined;
|
|
144
|
+
stripped.requestBodyOmitted = true;
|
|
145
|
+
}
|
|
146
|
+
if (stripped.responseData !== undefined) {
|
|
147
|
+
stripped.responseData = undefined;
|
|
148
|
+
stripped.responseBodyOmitted = true;
|
|
149
|
+
}
|
|
150
|
+
if (stripped.graphqlVariables !== undefined) {
|
|
151
|
+
stripped.graphqlVariables = toWireGraphqlVariables(stripped.graphqlVariables, /* force */true);
|
|
152
|
+
}
|
|
153
|
+
const entry = {
|
|
154
|
+
event: stripped,
|
|
155
|
+
bytes: approxJsonSize(stripped, SNAPSHOT_EVENTS_BUDGET).bytes
|
|
156
|
+
};
|
|
157
|
+
strippedCache.set(event, entry);
|
|
158
|
+
return entry;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Spend the snapshot budget newest-first. The store keeps events newest-first
|
|
163
|
+
* already, so this hands the developer full detail on what they are actually
|
|
164
|
+
* looking at and degrades the tail rather than dropping the whole panel.
|
|
165
|
+
*/
|
|
166
|
+
function fitEventsToBudget(events) {
|
|
167
|
+
const out = new Array(events.length);
|
|
168
|
+
let spent = 0;
|
|
169
|
+
for (let i = 0; i < events.length; i++) {
|
|
170
|
+
const full = toWireEvent(events[i]);
|
|
171
|
+
if (spent + full.bytes <= SNAPSHOT_EVENTS_BUDGET) {
|
|
172
|
+
out[i] = full.event;
|
|
173
|
+
spent += full.bytes;
|
|
174
|
+
} else {
|
|
175
|
+
const lean = toStrippedEvent(events[i]);
|
|
176
|
+
out[i] = lean.event;
|
|
177
|
+
spent += lean.bytes;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* GraphQL variables are copied off `request.data.variables` onto their own
|
|
185
|
+
* field for the list subtitle (`GetPokemon › Sandshrew`). Capping requestData
|
|
186
|
+
* still left that copy on the wire. Keep short primitives for the subtitle;
|
|
187
|
+
* drop nested/fat values (file uploads, base64). Full variables stay in
|
|
188
|
+
* `requestData` on-device and come back via `getEventBody`.
|
|
189
|
+
*/
|
|
190
|
+
function toWireGraphqlVariables(vars, force = false) {
|
|
191
|
+
if (!vars) return vars;
|
|
192
|
+
if (!force && !shouldStripBody(vars, undefined)) return vars;
|
|
193
|
+
const slim = {};
|
|
194
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
195
|
+
if (typeof value === "string" && value.length <= 64) slim[key] = value;else if (typeof value === "number" || typeof value === "boolean") {
|
|
196
|
+
slim[key] = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return Object.keys(slim).length > 0 ? slim : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Header values (Cookie, Authorization, custom JSON) still rode every
|
|
204
|
+
* snapshot after bodies were stripped. 500 events × a 4KB session cookie
|
|
205
|
+
* is a 2MB drop of the whole Network panel. Keep short values inline
|
|
206
|
+
* (content-type, accept); truncate the rest. Full maps stay on-device
|
|
207
|
+
* and come back via `getEventBody`.
|
|
208
|
+
*
|
|
209
|
+
* Returns the SAME object when nothing was truncated: callers use identity to
|
|
210
|
+
* decide whether anything was omitted, so a fresh `{}` here would flag every
|
|
211
|
+
* header-less event as truncated (and defeat the no-copy fast path).
|
|
212
|
+
*/
|
|
213
|
+
export const SNAPSHOT_HEADER_VALUE_LIMIT = 64;
|
|
214
|
+
function toWireHeaders(headers) {
|
|
215
|
+
if (!headers) return headers;
|
|
216
|
+
let changed = false;
|
|
217
|
+
const slim = {};
|
|
218
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
219
|
+
if (typeof value !== "string") continue;
|
|
220
|
+
if (value.length <= SNAPSHOT_HEADER_VALUE_LIMIT) {
|
|
221
|
+
slim[key] = value;
|
|
222
|
+
} else {
|
|
223
|
+
slim[key] = `${value.slice(0, SNAPSHOT_HEADER_VALUE_LIMIT)}… [${value.length - SNAPSHOT_HEADER_VALUE_LIMIT} more]`;
|
|
224
|
+
changed = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return changed ? slim : headers;
|
|
228
|
+
}
|
|
34
229
|
|
|
35
230
|
/**
|
|
36
231
|
* Drop oversized rule bodies from the snapshot, flagging what was dropped.
|
|
@@ -148,7 +343,9 @@ function baseFromRequest(params) {
|
|
|
148
343
|
* this package doesn't need a dependency on it).
|
|
149
344
|
*
|
|
150
345
|
* Subscribing starts the network interceptor, so requests are only captured
|
|
151
|
-
* while a dashboard is watching the network panel
|
|
346
|
+
* while a dashboard is watching the network panel — or while an MCP
|
|
347
|
+
* client holds a capture-mode watch (`devtool-watch` with `stream:false`),
|
|
348
|
+
* which records without asking the device to push snapshots.
|
|
152
349
|
*
|
|
153
350
|
* v2: large bodies are stripped from the snapshot and fetched lazily via
|
|
154
351
|
* `getEventBody` (see SNAPSHOT_BODY_INLINE_LIMIT).
|
|
@@ -160,9 +357,12 @@ function baseFromRequest(params) {
|
|
|
160
357
|
* `NetworkPanel`/`NetworkInline` accept either shape.
|
|
161
358
|
*/
|
|
162
359
|
export const networkSyncAdapter = {
|
|
163
|
-
|
|
360
|
+
// v5: honest request/responseSize + explicit request/responseBodyOmitted
|
|
361
|
+
// flags, and a total snapshot budget so the list degrades instead of the
|
|
362
|
+
// whole panel being dropped at the emit layer.
|
|
363
|
+
version: 5,
|
|
164
364
|
getSnapshot: () => ({
|
|
165
|
-
events: networkEventStore.getEvents()
|
|
365
|
+
events: fitEventsToBudget(networkEventStore.getEvents()),
|
|
166
366
|
/**
|
|
167
367
|
* Response-override rules live on the DEVICE — that's the only place they
|
|
168
368
|
* can run, since a rule is applied inside the app's own XMLHttpRequest.
|
|
@@ -174,7 +374,7 @@ export const networkSyncAdapter = {
|
|
|
174
374
|
// the same 200ms loop that the body-stripping exists to protect.
|
|
175
375
|
saved: networkSavedStore.getRecords().map(record => ({
|
|
176
376
|
...record,
|
|
177
|
-
event:
|
|
377
|
+
event: toWireEvent(record.event).event
|
|
178
378
|
}))
|
|
179
379
|
}),
|
|
180
380
|
subscribe: onChange => {
|
|
@@ -208,7 +408,9 @@ export const networkSyncAdapter = {
|
|
|
208
408
|
if (!event) return null;
|
|
209
409
|
return {
|
|
210
410
|
requestData: event.requestData ?? null,
|
|
211
|
-
responseData: event.responseData ?? null
|
|
411
|
+
responseData: event.responseData ?? null,
|
|
412
|
+
requestHeaders: event.requestHeaders ?? null,
|
|
413
|
+
responseHeaders: event.responseHeaders ?? null
|
|
212
414
|
};
|
|
213
415
|
},
|
|
214
416
|
/** Pin/unpin a request from the dashboard. */
|
|
@@ -355,6 +557,44 @@ export const networkSyncAdapter = {
|
|
|
355
557
|
};
|
|
356
558
|
},
|
|
357
559
|
/** Why isn't my rule firing? Reports the engine's own view of the world. */
|
|
560
|
+
/**
|
|
561
|
+
* Is the device actually recording right now, and why not?
|
|
562
|
+
*
|
|
563
|
+
* The honest answer to an empty request list. Without this a caller can
|
|
564
|
+
* only guess, and the plausible guesses ("interception is broken on this
|
|
565
|
+
* runtime") are all wrong — the usual answer is simply that nothing is
|
|
566
|
+
* holding capture open.
|
|
567
|
+
*/
|
|
568
|
+
getCaptureStatus: () => {
|
|
569
|
+
// RECORDING is what the caller actually wants to know, and it is driven
|
|
570
|
+
// by the store's subscriber count — not by whether the interceptor is
|
|
571
|
+
// installed. Those genuinely differ: an enabled override rule pins the
|
|
572
|
+
// interceptor (`overrideRulesStore.syncKeepAlive`) without anything
|
|
573
|
+
// writing to the event store, so reporting `isNetworkListening()` as
|
|
574
|
+
// "capturing" would answer "yes" to a device recording nothing. That is
|
|
575
|
+
// the same species of misleading answer this action exists to replace.
|
|
576
|
+
const subscribers = networkEventStore.getSubscriberCounts().total;
|
|
577
|
+
|
|
578
|
+
// `isNetworkListening()` lazily CONSTRUCTS the listener, which reaches
|
|
579
|
+
// for XMLHttpRequest — absent in Node and in any headless host. A
|
|
580
|
+
// diagnostic that throws is worse than no diagnostic, so probe
|
|
581
|
+
// defensively and report what we do know either way.
|
|
582
|
+
let interceptorInstalled = null;
|
|
583
|
+
let listenerCount = null;
|
|
584
|
+
try {
|
|
585
|
+
interceptorInstalled = isNetworkListening();
|
|
586
|
+
listenerCount = getNetworkListenerCount();
|
|
587
|
+
} catch {
|
|
588
|
+
/* no interceptable runtime here */
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
capturing: subscribers > 0,
|
|
592
|
+
subscribers,
|
|
593
|
+
interceptorInstalled,
|
|
594
|
+
listenerCount,
|
|
595
|
+
eventCount: networkEventStore.getEvents().length
|
|
596
|
+
};
|
|
597
|
+
},
|
|
358
598
|
debugOverrides: params => {
|
|
359
599
|
const url = (params && typeof params === "object" ? params.url : undefined) ?? "https://example.com/";
|
|
360
600
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkEventDetailView.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventDetailView.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,UAAU,CAAC;AA0B7D,UAAU,2BAA2B;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA+QD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"NetworkEventDetailView.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventDetailView.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,UAAU,CAAC;AA0B7D,UAAU,2BAA2B;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA+QD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,sBAAsB,mEAoiBjC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkEventDetailView.web.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventDetailView.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAWH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,UAAU,CAAC;AA8S7D,UAAU,2BAA2B;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAuOD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"NetworkEventDetailView.web.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkEventDetailView.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAWH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,UAAU,CAAC;AA8S7D,UAAU,2BAA2B;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAuOD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,mEAsfjC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"networkSyncAdapter.test.d.ts","sourceRoot":"","sources":["../../../../../src/network/sync/__tests__/networkSyncAdapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -6,6 +6,8 @@ import { type ReactNode } from "react";
|
|
|
6
6
|
export interface ResolvedNetworkBody {
|
|
7
7
|
requestData?: unknown;
|
|
8
8
|
responseData?: unknown;
|
|
9
|
+
requestHeaders?: Record<string, string> | null;
|
|
10
|
+
responseHeaders?: Record<string, string> | null;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Resolves request/response bodies that were omitted from a synced snapshot.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"networkBodyResolver.d.ts","sourceRoot":"","sources":["../../../../src/network/sync/networkBodyResolver.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"networkBodyResolver.d.ts","sourceRoot":"","sources":["../../../../src/network/sync/networkBodyResolver.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACjD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAChC,EAAE,EAAE,MAAM,KACP,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;AAMzC,wBAAgB,2BAA2B,CAAC,EAC1C,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAMA;AAED,wBAAgB,sBAAsB,IAAI,mBAAmB,GAAG,IAAI,CAEnE"}
|
|
@@ -2,23 +2,42 @@ import type { OverrideRulesState } from "../overrides/overrideRulesStore";
|
|
|
2
2
|
import type { OverrideRuleKind } from "../overrides/types";
|
|
3
3
|
import type { NetworkEvent } from "../types";
|
|
4
4
|
/**
|
|
5
|
-
* Request/response bodies larger than this
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* dashboard's main thread while it deserializes and re-renders them. Smaller
|
|
10
|
-
* bodies stay inline so the common case needs no round-trip (instant detail
|
|
11
|
-
* view, and they remain available to copy/export). Stripped bodies are fetched
|
|
12
|
-
* on demand via the `getEventBody` action when a request's detail view opens.
|
|
5
|
+
* Request/response bodies larger than this are NOT streamed in the list
|
|
6
|
+
* snapshot. Prefer the cached size proxy already on the event (free); if that
|
|
7
|
+
* field is missing or wrong (0 for a fat/cyclic body), fall back to an
|
|
8
|
+
* O(limit) walk. Stripped bodies come back via `getEventBody` on detail open.
|
|
13
9
|
*/
|
|
14
10
|
export declare const SNAPSHOT_BODY_INLINE_LIMIT: number;
|
|
11
|
+
/**
|
|
12
|
+
* Total inline budget for one snapshot's event list, held under the emit
|
|
13
|
+
* layer's 2MB so a snapshot is never DROPPED (a drop leaves the whole panel
|
|
14
|
+
* stale). Per-field caps alone don't get there: 500 requests with 9KB
|
|
15
|
+
* responses are all individually legal and still add up to ~4.7MB. Newest
|
|
16
|
+
* events keep their bodies inline (that's what a developer is looking at);
|
|
17
|
+
* once the budget is spent, older events degrade to metadata + fetch-on-demand.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SNAPSHOT_EVENTS_BUDGET: number;
|
|
20
|
+
/**
|
|
21
|
+
* Header values (Cookie, Authorization, custom JSON) still rode every
|
|
22
|
+
* snapshot after bodies were stripped. 500 events × a 4KB session cookie
|
|
23
|
+
* is a 2MB drop of the whole Network panel. Keep short values inline
|
|
24
|
+
* (content-type, accept); truncate the rest. Full maps stay on-device
|
|
25
|
+
* and come back via `getEventBody`.
|
|
26
|
+
*
|
|
27
|
+
* Returns the SAME object when nothing was truncated: callers use identity to
|
|
28
|
+
* decide whether anything was omitted, so a fresh `{}` here would flag every
|
|
29
|
+
* header-less event as truncated (and defeat the no-copy fast path).
|
|
30
|
+
*/
|
|
31
|
+
export declare const SNAPSHOT_HEADER_VALUE_LIMIT = 64;
|
|
15
32
|
/**
|
|
16
33
|
* Sync adapter for the network tool, consumed by @buoy-gg/external-sync's
|
|
17
34
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
18
35
|
* this package doesn't need a dependency on it).
|
|
19
36
|
*
|
|
20
37
|
* Subscribing starts the network interceptor, so requests are only captured
|
|
21
|
-
* while a dashboard is watching the network panel
|
|
38
|
+
* while a dashboard is watching the network panel — or while an MCP
|
|
39
|
+
* client holds a capture-mode watch (`devtool-watch` with `stream:false`),
|
|
40
|
+
* which records without asking the device to push snapshots.
|
|
22
41
|
*
|
|
23
42
|
* v2: large bodies are stripped from the snapshot and fetched lazily via
|
|
24
43
|
* `getEventBody` (see SNAPSHOT_BODY_INLINE_LIMIT).
|
|
@@ -59,12 +78,14 @@ export declare const networkSyncAdapter: {
|
|
|
59
78
|
* back to the saved store, which may hold the only remaining copy of a
|
|
60
79
|
* request that has since been cleared. Returns null if neither has it.
|
|
61
80
|
*/
|
|
62
|
-
getEventBody: (params
|
|
81
|
+
getEventBody: (params?: unknown) => {
|
|
63
82
|
requestData: {} | null;
|
|
64
83
|
responseData: {} | null;
|
|
84
|
+
requestHeaders: Record<string, string>;
|
|
85
|
+
responseHeaders: Record<string, string>;
|
|
65
86
|
} | null;
|
|
66
87
|
/** Pin/unpin a request from the dashboard. */
|
|
67
|
-
setPinned: (params
|
|
88
|
+
setPinned: (params?: unknown) => {
|
|
68
89
|
ok: true;
|
|
69
90
|
active: boolean;
|
|
70
91
|
} | {
|
|
@@ -74,7 +95,7 @@ export declare const networkSyncAdapter: {
|
|
|
74
95
|
ok: boolean;
|
|
75
96
|
} | null;
|
|
76
97
|
/** Save/unsave a request from the dashboard. */
|
|
77
|
-
setSaved: (params
|
|
98
|
+
setSaved: (params?: unknown) => {
|
|
78
99
|
ok: true;
|
|
79
100
|
active: boolean;
|
|
80
101
|
} | {
|
|
@@ -84,7 +105,7 @@ export declare const networkSyncAdapter: {
|
|
|
84
105
|
ok: boolean;
|
|
85
106
|
} | null;
|
|
86
107
|
/** Drop one pinned/saved record by its stable key. */
|
|
87
|
-
removeSavedRecord: (params
|
|
108
|
+
removeSavedRecord: (params?: unknown) => {
|
|
88
109
|
ok: boolean;
|
|
89
110
|
} | null;
|
|
90
111
|
clearSavedRequests: () => {
|
|
@@ -94,12 +115,12 @@ export declare const networkSyncAdapter: {
|
|
|
94
115
|
ok: boolean;
|
|
95
116
|
};
|
|
96
117
|
/** Master switch — turns every rule off without losing any of them. */
|
|
97
|
-
setOverridesEnabled: (params
|
|
118
|
+
setOverridesEnabled: (params?: unknown) => {
|
|
98
119
|
ok: boolean;
|
|
99
120
|
enabled: boolean;
|
|
100
121
|
};
|
|
101
122
|
/** Create a rule, or replace one by id. */
|
|
102
|
-
upsertOverrideRule: (params
|
|
123
|
+
upsertOverrideRule: (params?: unknown) => {
|
|
103
124
|
ok: boolean;
|
|
104
125
|
error: string;
|
|
105
126
|
rule?: undefined;
|
|
@@ -115,14 +136,14 @@ export declare const networkSyncAdapter: {
|
|
|
115
136
|
};
|
|
116
137
|
error?: undefined;
|
|
117
138
|
};
|
|
118
|
-
setOverrideRuleEnabled: (params
|
|
139
|
+
setOverrideRuleEnabled: (params?: unknown) => {
|
|
119
140
|
ok: boolean;
|
|
120
141
|
error: string;
|
|
121
142
|
} | {
|
|
122
143
|
ok: boolean;
|
|
123
144
|
error?: undefined;
|
|
124
145
|
};
|
|
125
|
-
deleteOverrideRule: (params
|
|
146
|
+
deleteOverrideRule: (params?: unknown) => {
|
|
126
147
|
ok: boolean;
|
|
127
148
|
error: string;
|
|
128
149
|
} | {
|
|
@@ -139,11 +160,26 @@ export declare const networkSyncAdapter: {
|
|
|
139
160
|
* (see `stripLargeRuleBodies`). Fetched when the user opens the rule, which
|
|
140
161
|
* is the only moment it's needed.
|
|
141
162
|
*/
|
|
142
|
-
getOverrideRuleBody: (params
|
|
163
|
+
getOverrideRuleBody: (params?: unknown) => {
|
|
143
164
|
body: string | null;
|
|
144
165
|
} | null;
|
|
145
166
|
/** Why isn't my rule firing? Reports the engine's own view of the world. */
|
|
146
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Is the device actually recording right now, and why not?
|
|
169
|
+
*
|
|
170
|
+
* The honest answer to an empty request list. Without this a caller can
|
|
171
|
+
* only guess, and the plausible guesses ("interception is broken on this
|
|
172
|
+
* runtime") are all wrong — the usual answer is simply that nothing is
|
|
173
|
+
* holding capture open.
|
|
174
|
+
*/
|
|
175
|
+
getCaptureStatus: () => {
|
|
176
|
+
capturing: boolean;
|
|
177
|
+
subscribers: number;
|
|
178
|
+
interceptorInstalled: boolean | null;
|
|
179
|
+
listenerCount: number | null;
|
|
180
|
+
eventCount: number;
|
|
181
|
+
};
|
|
182
|
+
debugOverrides: (params?: unknown) => {
|
|
147
183
|
interceptorInstalled: boolean;
|
|
148
184
|
listenerCount: number;
|
|
149
185
|
engine: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"networkSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/network/sync/networkSyncAdapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"networkSyncAdapter.d.ts","sourceRoot":"","sources":["../../../../src/network/sync/networkSyncAdapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAAY,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAAqB,CAAC;AA2LzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AA+J9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB;;;;QAO3B;;;;WAIG;;;;;;;;;;;;;0BAUiB,MAAM,IAAI;;;QAkB9B;;;;;WAKG;gCACqB,OAAO;;;;;;QAY/B,8CAA8C;6BACzB,OAAO;;;;;;;;;QAY5B,gDAAgD;4BAC5B,OAAO;;;;;;;;;QAU3B,sDAAsD;qCACzB,OAAO;;;;;;;;;QAsBpC,uEAAuE;uCACxC,OAAO;;;;QAKtC,2CAA2C;sCACb,OAAO;;;;;;;;;;;;;;;;0CAmCH,OAAO;;;;;;;sCAOX,OAAO;;;;;;;;;;QAYrC,+EAA+E;;QAG/E;;;;WAIG;uCAC4B,OAAO;;;QAYtC,4EAA4E;QAC5E;;;;;;;WAOG;;;;;;;;kCA+BuB,OAAO;;;;;;;;;;;;;;;;CAiBpC,CAAC"}
|
|
@@ -95,6 +95,20 @@ export interface NetworkEvent {
|
|
|
95
95
|
* // Display: "GetPokemon › Sandshrew"
|
|
96
96
|
*/
|
|
97
97
|
graphqlVariables?: Record<string, unknown>;
|
|
98
|
+
/**
|
|
99
|
+
* WIRE-ONLY: at least one header value was truncated on the snapshot.
|
|
100
|
+
* Full headers stay on-device and come back via `getEventBody`.
|
|
101
|
+
*/
|
|
102
|
+
headersOmitted?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* WIRE-ONLY: `requestData` was withheld from the snapshot (too large, not
|
|
105
|
+
* serializable, or past the snapshot's total budget). Sizes stay honest —
|
|
106
|
+
* this flag, not a doctored `requestSize`, is what marks fetch-on-demand.
|
|
107
|
+
* Mirrors `OverrideRule.bodyOmitted`. Fetched via `getEventBody`.
|
|
108
|
+
*/
|
|
109
|
+
requestBodyOmitted?: boolean;
|
|
110
|
+
/** WIRE-ONLY: `responseData` was withheld. See `requestBodyOmitted`. */
|
|
111
|
+
responseBodyOmitted?: boolean;
|
|
98
112
|
/**
|
|
99
113
|
* Set when a response-override rule affected this request.
|
|
100
114
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/network/types/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/network/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/network",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.18",
|
|
4
4
|
"description": "In-app network inspector for React Native — requests, responses, timing & errors live, with GraphQL operation parsing. Part of Buoy devtools.",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@buoy-gg/shared-ui": "^7.0.
|
|
29
|
+
"@buoy-gg/shared-ui": "^7.0.18"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "*",
|