@rozenite/network-activity-plugin 1.10.0 → 1.12.0
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/CHANGELOG.md +28 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-DsimzJvx.js → App-2rukIHdY.js} +1013 -264
- package/dist/devtools/assets/{App-CUXU0mup.css → App-xppYUJvX.css} +94 -0
- package/dist/rozenite.json +1 -1
- package/package.json +6 -6
- package/src/ui/components/FilterBar.tsx +13 -45
- package/src/ui/components/NetworkTimeline.tsx +422 -0
- package/src/ui/components/RequestList.tsx +6 -185
- package/src/ui/components/Toolbar.tsx +13 -1
- package/src/ui/hooks/useNetworkActivitySessionExport.ts +39 -0
- package/src/ui/state/__tests__/store.test.ts +77 -0
- package/src/ui/state/derived.ts +2 -0
- package/src/ui/state/filter.ts +49 -0
- package/src/ui/state/hooks.ts +2 -2
- package/src/ui/state/model.ts +1 -0
- package/src/ui/state/store.ts +24 -2
- package/src/ui/utils/__tests__/requestFilters.test.ts +32 -0
- package/src/ui/utils/__tests__/sessionExport.test.ts +174 -0
- package/src/ui/utils/__tests__/symbolication.test.ts +73 -0
- package/src/ui/utils/__tests__/timelineModel.test.ts +170 -0
- package/src/ui/utils/download.ts +7 -0
- package/src/ui/utils/requestFilters.ts +183 -0
- package/src/ui/utils/sessionExport.ts +185 -0
- package/src/ui/utils/symbolication.ts +37 -10
- package/src/ui/utils/timelineModel.ts +352 -0
- package/src/ui/views/InspectorView.tsx +40 -8
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { CSSProperties, PointerEvent } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import type { RequestId } from '../../shared/client';
|
|
5
|
+
import type { ProcessedRequest } from '../state/model';
|
|
6
|
+
import {
|
|
7
|
+
useNetworkActivityActions,
|
|
8
|
+
useSelectedRequestId,
|
|
9
|
+
} from '../state/hooks';
|
|
10
|
+
import {
|
|
11
|
+
formatTimelineOffset,
|
|
12
|
+
getTimelineBarTopOffset,
|
|
13
|
+
getTimelineModel,
|
|
14
|
+
getTimelineTrackTop,
|
|
15
|
+
isRequestActive,
|
|
16
|
+
TIMELINE_LAYOUT,
|
|
17
|
+
} from '../utils/timelineModel';
|
|
18
|
+
import type {
|
|
19
|
+
TimelineModel,
|
|
20
|
+
TimelineRangeSelection,
|
|
21
|
+
TimelineRow,
|
|
22
|
+
TimelineTick,
|
|
23
|
+
} from '../utils/timelineModel';
|
|
24
|
+
|
|
25
|
+
const REQUEST_TIMELINE_COLORS = {
|
|
26
|
+
error: 'bg-red-400',
|
|
27
|
+
primary: 'bg-gray-400',
|
|
28
|
+
active: 'bg-gray-500',
|
|
29
|
+
httpTtfb: 'bg-gray-200',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const getPrimaryBarClassName = (request: ProcessedRequest) => {
|
|
33
|
+
if (request.status === 'failed' || request.status === 'error') {
|
|
34
|
+
return REQUEST_TIMELINE_COLORS.error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return REQUEST_TIMELINE_COLORS.primary;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getStyle = (
|
|
41
|
+
offsetPercent: number,
|
|
42
|
+
widthPercent: number,
|
|
43
|
+
): CSSProperties => ({
|
|
44
|
+
left: `${offsetPercent}%`,
|
|
45
|
+
width: `${widthPercent}%`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const GridLines = ({ ticks }: { ticks: TimelineTick[] }) => {
|
|
49
|
+
return (
|
|
50
|
+
<div className="pointer-events-none absolute inset-0">
|
|
51
|
+
{ticks.map((tick) => (
|
|
52
|
+
<div
|
|
53
|
+
key={`${tick.label}-${tick.offsetPercent}`}
|
|
54
|
+
className="absolute inset-y-0 border-l border-gray-800"
|
|
55
|
+
style={{ left: `${tick.offsetPercent}%` }}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getTickLabelStyle = (tick: TimelineTick): CSSProperties => {
|
|
63
|
+
if (tick.offsetPercent === 0) {
|
|
64
|
+
return {
|
|
65
|
+
left: 4,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (tick.offsetPercent === 100) {
|
|
70
|
+
return {
|
|
71
|
+
right: 4,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
left: `${tick.offsetPercent}%`,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TimelineTrack = ({
|
|
81
|
+
row,
|
|
82
|
+
isSelected,
|
|
83
|
+
onSelect,
|
|
84
|
+
shouldSuppressSelect,
|
|
85
|
+
}: {
|
|
86
|
+
row: TimelineRow;
|
|
87
|
+
isSelected: boolean;
|
|
88
|
+
onSelect: (requestId: RequestId) => void;
|
|
89
|
+
shouldSuppressSelect: () => boolean;
|
|
90
|
+
}) => {
|
|
91
|
+
const primaryBarClassName = row.isActive
|
|
92
|
+
? REQUEST_TIMELINE_COLORS.active
|
|
93
|
+
: getPrimaryBarClassName(row.request);
|
|
94
|
+
const isSplitHttpBar =
|
|
95
|
+
row.request.type === 'http' &&
|
|
96
|
+
row.ttfbPercent > 0 &&
|
|
97
|
+
row.receivePercent > 0;
|
|
98
|
+
const trackTop = getTimelineTrackTop(row.lane);
|
|
99
|
+
const barTop = getTimelineBarTopOffset();
|
|
100
|
+
const positionStyle = {
|
|
101
|
+
...getStyle(row.offsetPercent, row.widthPercent),
|
|
102
|
+
top: trackTop,
|
|
103
|
+
};
|
|
104
|
+
const durationLabel = row.isActive
|
|
105
|
+
? `${formatTimelineOffset(row.duration)}+`
|
|
106
|
+
: formatTimelineOffset(row.duration);
|
|
107
|
+
const label = `${row.request.method} ${row.request.name} - ${durationLabel}`;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
aria-label={label}
|
|
113
|
+
title={label}
|
|
114
|
+
data-timeline-track="true"
|
|
115
|
+
className="absolute rounded-sm text-left transition-opacity hover:opacity-80"
|
|
116
|
+
style={{
|
|
117
|
+
...positionStyle,
|
|
118
|
+
height: TIMELINE_LAYOUT.laneHitTargetHeightPx,
|
|
119
|
+
}}
|
|
120
|
+
onClick={(event) => {
|
|
121
|
+
if (shouldSuppressSelect()) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onSelect(row.request.id);
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{isSplitHttpBar ? (
|
|
131
|
+
<div
|
|
132
|
+
className={`absolute flex w-full overflow-hidden rounded-sm ${
|
|
133
|
+
isSelected
|
|
134
|
+
? 'ring-1 ring-blue-300 ring-offset-1 ring-offset-gray-950'
|
|
135
|
+
: ''
|
|
136
|
+
}`}
|
|
137
|
+
style={{
|
|
138
|
+
top: barTop,
|
|
139
|
+
height: TIMELINE_LAYOUT.laneHeightPx,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<div
|
|
143
|
+
className={`h-full ${REQUEST_TIMELINE_COLORS.httpTtfb}`}
|
|
144
|
+
style={{ width: `${row.ttfbPercent}%` }}
|
|
145
|
+
/>
|
|
146
|
+
<div
|
|
147
|
+
className={`h-full ${REQUEST_TIMELINE_COLORS.primary}`}
|
|
148
|
+
style={{ width: `${row.receivePercent}%` }}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
) : (
|
|
152
|
+
<div
|
|
153
|
+
className={`absolute w-full rounded-sm ${primaryBarClassName} ${
|
|
154
|
+
isSelected
|
|
155
|
+
? 'ring-1 ring-blue-300 ring-offset-1 ring-offset-gray-950'
|
|
156
|
+
: ''
|
|
157
|
+
}`}
|
|
158
|
+
style={{
|
|
159
|
+
top: barTop,
|
|
160
|
+
height: TIMELINE_LAYOUT.laneHeightPx,
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
type DraftSelection = {
|
|
169
|
+
anchorPercent: number;
|
|
170
|
+
currentPercent: number;
|
|
171
|
+
startedOnTrack: boolean;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const clampPercent = (value: number) => Math.min(Math.max(value, 0), 100);
|
|
175
|
+
|
|
176
|
+
const getPointerPercent = (
|
|
177
|
+
event: PointerEvent<HTMLDivElement>,
|
|
178
|
+
element: HTMLDivElement,
|
|
179
|
+
) => {
|
|
180
|
+
const rect = element.getBoundingClientRect();
|
|
181
|
+
|
|
182
|
+
if (rect.width === 0) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return clampPercent(((event.clientX - rect.left) / rect.width) * 100);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const getSelectionStyle = (
|
|
190
|
+
range: TimelineRangeSelection,
|
|
191
|
+
timeline: TimelineModel,
|
|
192
|
+
): CSSProperties => {
|
|
193
|
+
const startPercent = clampPercent(
|
|
194
|
+
((range.startTime - timeline.rangeStart) / timeline.rangeDuration) * 100,
|
|
195
|
+
);
|
|
196
|
+
const endPercent = clampPercent(
|
|
197
|
+
((range.endTime - timeline.rangeStart) / timeline.rangeDuration) * 100,
|
|
198
|
+
);
|
|
199
|
+
const left = Math.min(startPercent, endPercent);
|
|
200
|
+
const width = Math.abs(endPercent - startPercent);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
left: `${left}%`,
|
|
204
|
+
width: `${width}%`,
|
|
205
|
+
top: TIMELINE_LAYOUT.rulerHeightPx,
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const getDraftSelectionStyle = (draft: DraftSelection): CSSProperties => {
|
|
210
|
+
const left = Math.min(draft.anchorPercent, draft.currentPercent);
|
|
211
|
+
const width = Math.abs(draft.currentPercent - draft.anchorPercent);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
left: `${left}%`,
|
|
215
|
+
width: `${width}%`,
|
|
216
|
+
top: TIMELINE_LAYOUT.rulerHeightPx,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type NetworkTimelineProps = {
|
|
221
|
+
requests: ProcessedRequest[];
|
|
222
|
+
selection: TimelineRangeSelection | null;
|
|
223
|
+
filteredRequestCount: number;
|
|
224
|
+
onSelectionChange: (selection: TimelineRangeSelection | null) => void;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const NetworkTimeline = ({
|
|
228
|
+
requests,
|
|
229
|
+
selection,
|
|
230
|
+
filteredRequestCount,
|
|
231
|
+
onSelectionChange,
|
|
232
|
+
}: NetworkTimelineProps) => {
|
|
233
|
+
const actions = useNetworkActivityActions();
|
|
234
|
+
const selectedRequestId = useSelectedRequestId();
|
|
235
|
+
const [now, setNow] = useState(() => Date.now());
|
|
236
|
+
const [draftSelection, setDraftSelection] = useState<DraftSelection | null>(
|
|
237
|
+
null,
|
|
238
|
+
);
|
|
239
|
+
const chartRef = useRef<HTMLDivElement | null>(null);
|
|
240
|
+
const suppressTrackClickRef = useRef(false);
|
|
241
|
+
|
|
242
|
+
const hasActiveRequests = requests.some(isRequestActive);
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!hasActiveRequests) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const interval = window.setInterval(() => {
|
|
250
|
+
setNow(Date.now());
|
|
251
|
+
}, TIMELINE_LAYOUT.liveRefreshMs);
|
|
252
|
+
|
|
253
|
+
return () => window.clearInterval(interval);
|
|
254
|
+
}, [hasActiveRequests]);
|
|
255
|
+
|
|
256
|
+
const timeline = useMemo(() => {
|
|
257
|
+
return getTimelineModel(requests, now);
|
|
258
|
+
}, [requests, now]);
|
|
259
|
+
|
|
260
|
+
const onRequestSelect = (requestId: RequestId) => {
|
|
261
|
+
actions.setSelectedRequest(requestId);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
265
|
+
if (event.button !== 0 || requests.length === 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const chartElement = chartRef.current;
|
|
270
|
+
|
|
271
|
+
if (!chartElement) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const percent = getPointerPercent(event, chartElement);
|
|
276
|
+
const target = event.target;
|
|
277
|
+
const startedOnTrack =
|
|
278
|
+
target instanceof Element &&
|
|
279
|
+
target.closest('[data-timeline-track="true"]') !== null;
|
|
280
|
+
|
|
281
|
+
setDraftSelection({
|
|
282
|
+
anchorPercent: percent,
|
|
283
|
+
currentPercent: percent,
|
|
284
|
+
startedOnTrack,
|
|
285
|
+
});
|
|
286
|
+
chartElement.setPointerCapture(event.pointerId);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
|
290
|
+
if (!draftSelection) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const chartElement = chartRef.current;
|
|
295
|
+
|
|
296
|
+
if (!chartElement) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
const percent = getPointerPercent(event, chartElement);
|
|
302
|
+
|
|
303
|
+
setDraftSelection((current) =>
|
|
304
|
+
current ? { ...current, currentPercent: percent } : current,
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const onPointerUp = (event: PointerEvent<HTMLDivElement>) => {
|
|
309
|
+
if (!draftSelection) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const chartElement = chartRef.current;
|
|
314
|
+
const currentPercent = chartElement
|
|
315
|
+
? getPointerPercent(event, chartElement)
|
|
316
|
+
: draftSelection.currentPercent;
|
|
317
|
+
const distance = Math.abs(currentPercent - draftSelection.anchorPercent);
|
|
318
|
+
|
|
319
|
+
if (distance > 1) {
|
|
320
|
+
const startOffset =
|
|
321
|
+
(Math.min(draftSelection.anchorPercent, currentPercent) / 100) *
|
|
322
|
+
timeline.rangeDuration;
|
|
323
|
+
const endOffset =
|
|
324
|
+
(Math.max(draftSelection.anchorPercent, currentPercent) / 100) *
|
|
325
|
+
timeline.rangeDuration;
|
|
326
|
+
|
|
327
|
+
onSelectionChange({
|
|
328
|
+
startTime: timeline.rangeStart + startOffset,
|
|
329
|
+
endTime: timeline.rangeStart + endOffset,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
suppressTrackClickRef.current = true;
|
|
333
|
+
window.setTimeout(() => {
|
|
334
|
+
suppressTrackClickRef.current = false;
|
|
335
|
+
}, 0);
|
|
336
|
+
} else if (!draftSelection.startedOnTrack) {
|
|
337
|
+
onSelectionChange(null);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setDraftSelection(null);
|
|
341
|
+
|
|
342
|
+
if (chartElement?.hasPointerCapture(event.pointerId)) {
|
|
343
|
+
chartElement.releasePointerCapture(event.pointerId);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div className="border-b border-gray-700 bg-gray-900 p-1.5">
|
|
349
|
+
<div
|
|
350
|
+
ref={chartRef}
|
|
351
|
+
className="relative overflow-hidden border border-gray-800 bg-gray-950"
|
|
352
|
+
style={{ height: timeline.chartHeight }}
|
|
353
|
+
onPointerDown={onPointerDown}
|
|
354
|
+
onPointerMove={onPointerMove}
|
|
355
|
+
onPointerUp={onPointerUp}
|
|
356
|
+
>
|
|
357
|
+
<GridLines ticks={timeline.ticks} />
|
|
358
|
+
|
|
359
|
+
<div
|
|
360
|
+
className="pointer-events-none absolute inset-x-0 border-b border-gray-800"
|
|
361
|
+
style={{ top: TIMELINE_LAYOUT.rulerHeightPx }}
|
|
362
|
+
/>
|
|
363
|
+
|
|
364
|
+
{timeline.ticks.map((tick) => (
|
|
365
|
+
<div
|
|
366
|
+
key={`${tick.label}-${tick.offsetPercent}`}
|
|
367
|
+
className="absolute top-1 whitespace-nowrap tabular-nums text-xs text-gray-200"
|
|
368
|
+
style={getTickLabelStyle(tick)}
|
|
369
|
+
>
|
|
370
|
+
{tick.label}
|
|
371
|
+
</div>
|
|
372
|
+
))}
|
|
373
|
+
|
|
374
|
+
{selection && (
|
|
375
|
+
<div
|
|
376
|
+
className="pointer-events-none absolute bottom-0 border-x border-blue-300/70 bg-blue-400/10"
|
|
377
|
+
style={getSelectionStyle(selection, timeline)}
|
|
378
|
+
/>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{draftSelection && (
|
|
382
|
+
<div
|
|
383
|
+
className="pointer-events-none absolute bottom-0 border-x border-blue-300/70 bg-blue-400/15"
|
|
384
|
+
style={getDraftSelectionStyle(draftSelection)}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{timeline.rows.map((row) => (
|
|
389
|
+
<TimelineTrack
|
|
390
|
+
key={row.request.id}
|
|
391
|
+
row={row}
|
|
392
|
+
isSelected={selectedRequestId === row.request.id}
|
|
393
|
+
onSelect={onRequestSelect}
|
|
394
|
+
shouldSuppressSelect={() => suppressTrackClickRef.current}
|
|
395
|
+
/>
|
|
396
|
+
))}
|
|
397
|
+
|
|
398
|
+
{selection && (
|
|
399
|
+
<div className="absolute bottom-1 right-1 flex items-center gap-1 rounded border border-gray-700 bg-gray-900/95 px-1.5 py-0.5 text-xs text-gray-400">
|
|
400
|
+
<span>{filteredRequestCount} in range</span>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
title="Clear timeline selection"
|
|
404
|
+
aria-label="Clear timeline selection"
|
|
405
|
+
className="rounded p-0.5 text-gray-400 hover:bg-gray-800 hover:text-gray-100"
|
|
406
|
+
onClick={() => onSelectionChange(null)}
|
|
407
|
+
>
|
|
408
|
+
<X className="h-3 w-3" />
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
{timeline.hiddenRequestCount > 0 && (
|
|
414
|
+
<div className="absolute bottom-1 left-1 rounded border border-gray-700 bg-gray-900/95 px-1.5 py-0.5 text-xs text-gray-400">
|
|
415
|
+
Showing latest {timeline.rows.length} of{' '}
|
|
416
|
+
{timeline.totalRequestCount}
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
} from '@tanstack/react-table';
|
|
11
11
|
import type { ProcessedRequest } from '../state/model';
|
|
12
12
|
import type {
|
|
13
|
-
HttpMethod,
|
|
14
13
|
NetworkEventSource,
|
|
15
14
|
RequestId,
|
|
16
15
|
RequestOverride,
|
|
@@ -18,12 +17,10 @@ import type {
|
|
|
18
17
|
import {
|
|
19
18
|
useNetworkActivityActions,
|
|
20
19
|
useOverrides,
|
|
21
|
-
useProcessedRequests,
|
|
22
20
|
useSelectedRequestId,
|
|
23
21
|
useClientUISettings,
|
|
24
22
|
} from '../state/hooks';
|
|
25
23
|
import { getStatusColor } from '../utils/getStatusColor';
|
|
26
|
-
import { FilterState } from './FilterBar';
|
|
27
24
|
import { isNumber } from '../../utils/typeChecks';
|
|
28
25
|
|
|
29
26
|
type NetworkRequest = {
|
|
@@ -31,7 +28,6 @@ type NetworkRequest = {
|
|
|
31
28
|
name: string;
|
|
32
29
|
status: string | number;
|
|
33
30
|
statusCode?: number;
|
|
34
|
-
statusState: ProcessedRequest['status'];
|
|
35
31
|
method: ProcessedRequest['method'];
|
|
36
32
|
domain: string;
|
|
37
33
|
path: string;
|
|
@@ -114,7 +110,6 @@ const sortSize: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
|
114
110
|
const a = rowA.getValue(columnId) as string;
|
|
115
111
|
const b = rowB.getValue(columnId) as string;
|
|
116
112
|
|
|
117
|
-
// Extract numeric values from formatted strings like "1.2 kB", "500 B", etc.
|
|
118
113
|
const getNumericValue = (str: string) => {
|
|
119
114
|
const match = str.match(/^([\d.]+)\s*([KMGT]?B)$/);
|
|
120
115
|
if (!match) return 0;
|
|
@@ -136,7 +131,6 @@ const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
|
136
131
|
const a = rowA.getValue(columnId) as string;
|
|
137
132
|
const b = rowB.getValue(columnId) as string;
|
|
138
133
|
|
|
139
|
-
// Extract numeric values from formatted strings like "150 ms", "1.2 s", etc.
|
|
140
134
|
const getNumericValue = (str: string) => {
|
|
141
135
|
const match = str.match(/^([\d.]+)\s*(ms|s)$/);
|
|
142
136
|
if (!match) return 0;
|
|
@@ -148,175 +142,6 @@ const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
|
148
142
|
return getNumericValue(a) - getNumericValue(b);
|
|
149
143
|
};
|
|
150
144
|
|
|
151
|
-
const parseThreshold = (value: string): number | null => {
|
|
152
|
-
const normalizedValue = value.trim();
|
|
153
|
-
if (!normalizedValue) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const parsedValue = Number(normalizedValue);
|
|
158
|
-
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const matchesStatusFilter = (
|
|
162
|
-
statusCode: number | undefined,
|
|
163
|
-
statusFilter: string,
|
|
164
|
-
) => {
|
|
165
|
-
const normalizedFilter = statusFilter.trim().toLowerCase();
|
|
166
|
-
if (!normalizedFilter) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (statusCode === undefined) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const statusRangeMatch = normalizedFilter.match(/^(\d{3})\s*-\s*(\d{3})$/);
|
|
175
|
-
if (statusRangeMatch) {
|
|
176
|
-
const min = Number(statusRangeMatch[1]);
|
|
177
|
-
const max = Number(statusRangeMatch[2]);
|
|
178
|
-
return statusCode >= min && statusCode <= max;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const statusClassMatch = normalizedFilter.match(/^([1-5])xx$/);
|
|
182
|
-
if (statusClassMatch) {
|
|
183
|
-
return Math.floor(statusCode / 100) === Number(statusClassMatch[1]);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const comparisonMatch = normalizedFilter.match(/^(>=|<=|>|<)\s*(\d{3})$/);
|
|
187
|
-
if (comparisonMatch) {
|
|
188
|
-
const value = Number(comparisonMatch[2]);
|
|
189
|
-
switch (comparisonMatch[1]) {
|
|
190
|
-
case '>=':
|
|
191
|
-
return statusCode >= value;
|
|
192
|
-
case '<=':
|
|
193
|
-
return statusCode <= value;
|
|
194
|
-
case '>':
|
|
195
|
-
return statusCode > value;
|
|
196
|
-
case '<':
|
|
197
|
-
return statusCode < value;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return statusCode === Number(normalizedFilter);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const isInFlightStatus = (status: string) => {
|
|
205
|
-
return ['pending', 'loading', 'connecting', 'open'].includes(status);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const isFailedStatus = (status: string) => {
|
|
209
|
-
return ['failed', 'error'].includes(status);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const isHttpMethod = (method: NetworkRequest['method']): method is HttpMethod =>
|
|
213
|
-
method !== 'WS' && method !== 'SSE';
|
|
214
|
-
|
|
215
|
-
const filterNetworkRequests = (
|
|
216
|
-
requests: NetworkRequest[],
|
|
217
|
-
filter: FilterState,
|
|
218
|
-
) => {
|
|
219
|
-
const searchText = filter.text.trim().toLowerCase();
|
|
220
|
-
const domainFilter = filter.advanced.domain.trim().toLowerCase();
|
|
221
|
-
const contentTypeFilter = filter.advanced.contentType.trim().toLowerCase();
|
|
222
|
-
const minSize = parseThreshold(filter.advanced.minSize);
|
|
223
|
-
const maxSize = parseThreshold(filter.advanced.maxSize);
|
|
224
|
-
const minDuration = parseThreshold(filter.advanced.minDuration);
|
|
225
|
-
const maxDuration = parseThreshold(filter.advanced.maxDuration);
|
|
226
|
-
|
|
227
|
-
return requests.filter((request) => {
|
|
228
|
-
if (filter.types.size > 0 && !filter.types.has(request.type)) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
filter.advanced.methods.size > 0 &&
|
|
234
|
-
(!isHttpMethod(request.method) ||
|
|
235
|
-
!filter.advanced.methods.has(request.method))
|
|
236
|
-
) {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
filter.advanced.sources.size > 0 &&
|
|
242
|
-
(!request.source || !filter.advanced.sources.has(request.source))
|
|
243
|
-
) {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!matchesStatusFilter(request.statusCode, filter.advanced.status)) {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (domainFilter && !request.domain.toLowerCase().includes(domainFilter)) {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
contentTypeFilter &&
|
|
257
|
-
!request.contentType?.toLowerCase().includes(contentTypeFilter)
|
|
258
|
-
) {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (filter.advanced.failedOnly && !isFailedStatus(request.statusState)) {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
filter.advanced.inFlightOnly &&
|
|
268
|
-
!isInFlightStatus(request.statusState)
|
|
269
|
-
) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (filter.advanced.overriddenOnly && !request.hasOverride) {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
minSize !== null &&
|
|
279
|
-
(request.sizeBytes === null || request.sizeBytes < minSize)
|
|
280
|
-
) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
maxSize !== null &&
|
|
286
|
-
(request.sizeBytes === null || request.sizeBytes > maxSize)
|
|
287
|
-
) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (minDuration !== null && request.durationMs < minDuration) {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (maxDuration !== null && request.durationMs > maxDuration) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (searchText) {
|
|
300
|
-
const searchableFields = [
|
|
301
|
-
request.name,
|
|
302
|
-
request.method,
|
|
303
|
-
request.status,
|
|
304
|
-
request.domain,
|
|
305
|
-
request.path,
|
|
306
|
-
request.source,
|
|
307
|
-
request.type,
|
|
308
|
-
request.contentType,
|
|
309
|
-
]
|
|
310
|
-
.join(' ')
|
|
311
|
-
.toLowerCase();
|
|
312
|
-
|
|
313
|
-
return searchableFields.includes(searchText);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return true;
|
|
317
|
-
});
|
|
318
|
-
};
|
|
319
|
-
|
|
320
145
|
const processNetworkRequests = (
|
|
321
146
|
processedRequests: ProcessedRequest[],
|
|
322
147
|
overrides: Map<string, RequestOverride>,
|
|
@@ -340,7 +165,6 @@ const processNetworkRequests = (
|
|
|
340
165
|
name: generateName(request.name, showEntirePathAsName),
|
|
341
166
|
status: statusDisplay,
|
|
342
167
|
statusCode: request.httpStatus || undefined,
|
|
343
|
-
statusState: request.status,
|
|
344
168
|
method: request.method,
|
|
345
169
|
domain,
|
|
346
170
|
path,
|
|
@@ -352,7 +176,7 @@ const processNetworkRequests = (
|
|
|
352
176
|
type: request.type,
|
|
353
177
|
source: request.source,
|
|
354
178
|
startTime: formatStartTime(request.timestamp),
|
|
355
|
-
hasOverride
|
|
179
|
+
hasOverride,
|
|
356
180
|
};
|
|
357
181
|
});
|
|
358
182
|
};
|
|
@@ -428,25 +252,23 @@ const columns = [
|
|
|
428
252
|
];
|
|
429
253
|
|
|
430
254
|
export type RequestListProps = {
|
|
431
|
-
|
|
255
|
+
requests: ProcessedRequest[];
|
|
432
256
|
};
|
|
433
257
|
|
|
434
|
-
export const RequestList = ({
|
|
258
|
+
export const RequestList = ({ requests: filteredRequests }: RequestListProps) => {
|
|
435
259
|
const actions = useNetworkActivityActions();
|
|
436
|
-
const processedRequests = useProcessedRequests();
|
|
437
260
|
const selectedRequestId = useSelectedRequestId();
|
|
438
261
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
439
262
|
const overrides = useOverrides();
|
|
440
263
|
const clientUISettings = useClientUISettings();
|
|
441
264
|
|
|
442
265
|
const requests = useMemo(() => {
|
|
443
|
-
|
|
444
|
-
|
|
266
|
+
return processNetworkRequests(
|
|
267
|
+
filteredRequests,
|
|
445
268
|
overrides,
|
|
446
269
|
clientUISettings?.showUrlAsName,
|
|
447
270
|
);
|
|
448
|
-
|
|
449
|
-
}, [processedRequests, overrides, clientUISettings?.showUrlAsName, filter]);
|
|
271
|
+
}, [filteredRequests, overrides, clientUISettings?.showUrlAsName]);
|
|
450
272
|
|
|
451
273
|
const table = useReactTable({
|
|
452
274
|
data: requests,
|
|
@@ -527,7 +349,6 @@ export const RequestList = ({ filter }: RequestListProps) => {
|
|
|
527
349
|
);
|
|
528
350
|
};
|
|
529
351
|
|
|
530
|
-
// Export helper functions for use in other components
|
|
531
352
|
export {
|
|
532
353
|
formatSize,
|
|
533
354
|
formatDuration,
|