@buoy-gg/network 7.0.17 → 7.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/lib/commonjs/network/components/NetworkEventDetailView.js +18 -12
  2. package/lib/commonjs/network/components/NetworkEventDetailView.web.js +18 -12
  3. package/lib/commonjs/network/sync/networkSyncAdapter.js +265 -26
  4. package/lib/module/network/components/NetworkEventDetailView.js +18 -12
  5. package/lib/module/network/components/NetworkEventDetailView.web.js +18 -12
  6. package/lib/module/network/sync/networkSyncAdapter.js +265 -25
  7. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  8. package/lib/typescript/network/components/NetworkEventDetailView.web.d.ts.map +1 -1
  9. package/lib/typescript/network/sync/networkBodyResolver.d.ts +2 -0
  10. package/lib/typescript/network/sync/networkBodyResolver.d.ts.map +1 -1
  11. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +55 -19
  12. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  13. package/lib/typescript/network/types/index.d.ts +14 -0
  14. package/lib/typescript/network/types/index.d.ts.map +1 -1
  15. package/package.json +8 -3
  16. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +0 -16
  17. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +0 -1
  18. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +0 -19
  19. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +0 -1
  20. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +0 -22
  21. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +0 -1
@@ -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
- const requestBodyOmitted = event.requestData === undefined && (event.requestSize ?? 0) > 0;
328
- const responseBodyOmitted = event.responseData === undefined && (event.responseSize ?? 0) > 0;
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: event.requestHeaders,
369
+ requestHeaders,
364
370
  requestData,
365
- responseHeaders: event.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(event.requestHeaders).length,
555
- children: Object.keys(event.requestHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
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: event.requestHeaders,
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(event.responseHeaders).length,
577
- children: Object.keys(event.responseHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
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: event.responseHeaders,
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
- const requestBodyOmitted = event.requestData === undefined && (event.requestSize ?? 0) > 0;
641
- const responseBodyOmitted = event.responseData === undefined && (event.responseSize ?? 0) > 0;
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: event.requestHeaders,
683
+ requestHeaders,
678
684
  requestData,
679
- responseHeaders: event.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(event.requestHeaders).length,
853
- children: Object.keys(event.requestHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
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: event.requestHeaders,
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(event.responseHeaders).length,
874
- children: Object.keys(event.responseHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
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: event.responseHeaders,
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 (using the cached size proxy already
15
- * on the event — no per-snapshot stringify) are NOT streamed in the list
16
- * snapshot. These are the multi-MB payloads (images, large JSON list responses)
17
- * that, re-sent every ~200ms for up to 500 events, freeze the desktop
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
- const stripLargeBodies = event => {
25
- const stripRequest = event.requestData !== undefined && (event.requestSize ?? 0) > SNAPSHOT_BODY_INLINE_LIMIT;
26
- const stripResponse = event.responseData !== undefined && (event.responseSize ?? 0) > SNAPSHOT_BODY_INLINE_LIMIT;
27
- if (!stripRequest && !stripResponse) return event;
28
- // Keep sizes/headers/metadata; only drop the heavy data. `undefined` keys are
29
- // omitted by JSON serialization, so this shrinks the wire payload, and the
30
- // dashboard treats "data missing but size > 0" as "fetch on demand".
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
- if (stripRequest) stripped.requestData = undefined;
35
- if (stripResponse) stripped.responseData = undefined;
36
- return stripped;
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
- version: 4,
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().map(stripLargeBodies),
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: stripLargeBodies(record.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
- const requestBodyOmitted = event.requestData === undefined && (event.requestSize ?? 0) > 0;
327
- const responseBodyOmitted = event.responseData === undefined && (event.responseSize ?? 0) > 0;
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: event.requestHeaders,
368
+ requestHeaders,
363
369
  requestData,
364
- responseHeaders: event.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(event.requestHeaders).length,
554
- children: Object.keys(event.requestHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
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: event.requestHeaders,
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(event.responseHeaders).length,
576
- children: Object.keys(event.responseHeaders).length > 0 ? /*#__PURE__*/_jsx(View, {
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: event.responseHeaders,
586
+ data: responseHeaders,
581
587
  showTypeFilter: true,
582
588
  rawMode: true,
583
589
  initialExpanded: true,