@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Button } from './Button';
|
|
2
|
-
import { Circle, Square, Trash2 } from 'lucide-react';
|
|
2
|
+
import { Circle, Download, Square, Trash2 } from 'lucide-react';
|
|
3
3
|
import { useIsRecording, useNetworkActivityActions } from '../state/hooks';
|
|
4
|
+
import { useNetworkActivitySessionExport } from '../hooks/useNetworkActivitySessionExport';
|
|
4
5
|
|
|
5
6
|
export const Toolbar = () => {
|
|
6
7
|
const actions = useNetworkActivityActions();
|
|
7
8
|
const isRecording = useIsRecording();
|
|
9
|
+
const { canExportSession, exportSession } = useNetworkActivitySessionExport();
|
|
8
10
|
|
|
9
11
|
const onToggleRecording = (): void => {
|
|
10
12
|
actions.setRecording(!isRecording);
|
|
@@ -41,6 +43,16 @@ export const Toolbar = () => {
|
|
|
41
43
|
>
|
|
42
44
|
<Trash2 className="h-4 w-4" />
|
|
43
45
|
</Button>
|
|
46
|
+
<Button
|
|
47
|
+
variant="ghost"
|
|
48
|
+
size="sm"
|
|
49
|
+
onClick={exportSession}
|
|
50
|
+
disabled={!canExportSession}
|
|
51
|
+
className="ml-auto h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
|
|
52
|
+
title="Export session"
|
|
53
|
+
>
|
|
54
|
+
<Download className="h-4 w-4" />
|
|
55
|
+
</Button>
|
|
44
56
|
</div>
|
|
45
57
|
);
|
|
46
58
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useNetworkActivityStore } from '../state/hooks';
|
|
3
|
+
import { store } from '../state/store';
|
|
4
|
+
import { downloadJson } from '../utils/download';
|
|
5
|
+
import {
|
|
6
|
+
createNetworkActivitySessionExport,
|
|
7
|
+
getNetworkActivitySessionExportFileName,
|
|
8
|
+
} from '../utils/sessionExport';
|
|
9
|
+
|
|
10
|
+
export const useNetworkActivitySessionExport = () => {
|
|
11
|
+
const canExportSession = useNetworkActivityStore(
|
|
12
|
+
(state) => state.networkEntries.size > 0,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const exportSession = useCallback(() => {
|
|
16
|
+
const { networkEntries, websocketMessages } = store.getState();
|
|
17
|
+
|
|
18
|
+
if (networkEntries.size === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const exportedAt = new Date();
|
|
23
|
+
const exportData = createNetworkActivitySessionExport(
|
|
24
|
+
networkEntries,
|
|
25
|
+
websocketMessages,
|
|
26
|
+
exportedAt,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
downloadJson(
|
|
30
|
+
exportData,
|
|
31
|
+
getNetworkActivitySessionExportFileName(exportedAt),
|
|
32
|
+
);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
canExportSession,
|
|
37
|
+
exportSession,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { createNetworkActivityStore as createStoreType } from '../store';
|
|
3
|
+
|
|
4
|
+
let createNetworkActivityStore: typeof createStoreType;
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
const storage = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
vi.stubGlobal('localStorage', {
|
|
10
|
+
getItem: (key: string) => storage.get(key) ?? null,
|
|
11
|
+
setItem: (key: string, value: string) => {
|
|
12
|
+
storage.set(key, value);
|
|
13
|
+
},
|
|
14
|
+
removeItem: (key: string) => {
|
|
15
|
+
storage.delete(key);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
({ createNetworkActivityStore } = await import('../store'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('network activity store', () => {
|
|
23
|
+
it('records elapsed duration for failed HTTP requests', () => {
|
|
24
|
+
const store = createNetworkActivityStore();
|
|
25
|
+
|
|
26
|
+
store.getState().handleEvent('request-sent', {
|
|
27
|
+
requestId: 'request-1',
|
|
28
|
+
timestamp: 100,
|
|
29
|
+
request: {
|
|
30
|
+
url: 'https://example.com/api',
|
|
31
|
+
method: 'GET',
|
|
32
|
+
headers: {},
|
|
33
|
+
},
|
|
34
|
+
initiator: {
|
|
35
|
+
type: 'script',
|
|
36
|
+
},
|
|
37
|
+
type: 'Fetch',
|
|
38
|
+
});
|
|
39
|
+
store.getState().handleEvent('request-failed', {
|
|
40
|
+
requestId: 'request-1',
|
|
41
|
+
timestamp: 250,
|
|
42
|
+
type: 'Fetch',
|
|
43
|
+
error: 'Network request failed',
|
|
44
|
+
canceled: false,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(store.getState().networkEntries.get('request-1')).toMatchObject({
|
|
48
|
+
status: 'failed',
|
|
49
|
+
duration: 150,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('records elapsed duration for websocket errors', () => {
|
|
54
|
+
const store = createNetworkActivityStore();
|
|
55
|
+
|
|
56
|
+
store.getState().handleEvent('websocket-connect', {
|
|
57
|
+
type: 'websocket-connect',
|
|
58
|
+
url: 'wss://example.com/socket',
|
|
59
|
+
socketId: 'socket-1',
|
|
60
|
+
timestamp: 100,
|
|
61
|
+
protocols: null,
|
|
62
|
+
options: [],
|
|
63
|
+
});
|
|
64
|
+
store.getState().handleEvent('websocket-error', {
|
|
65
|
+
type: 'websocket-error',
|
|
66
|
+
url: 'wss://example.com/socket',
|
|
67
|
+
socketId: 'socket-1',
|
|
68
|
+
timestamp: 175,
|
|
69
|
+
error: 'Socket failed',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(store.getState().networkEntries.get('ws-socket-1')).toMatchObject({
|
|
73
|
+
status: 'error',
|
|
74
|
+
duration: 75,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/ui/state/derived.ts
CHANGED
|
@@ -27,6 +27,7 @@ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
|
|
|
27
27
|
method: httpEntry.request.method,
|
|
28
28
|
httpStatus: httpEntry.response?.status,
|
|
29
29
|
contentType: httpEntry.response?.contentType,
|
|
30
|
+
ttfb: httpEntry.ttfb,
|
|
30
31
|
progress: httpEntry.progress,
|
|
31
32
|
});
|
|
32
33
|
} else if (entry.type === 'websocket') {
|
|
@@ -95,6 +96,7 @@ export const getRequestSummary = (
|
|
|
95
96
|
method: httpEntry.request.method,
|
|
96
97
|
httpStatus: httpEntry.response?.status || 0,
|
|
97
98
|
contentType: httpEntry.response?.contentType,
|
|
99
|
+
ttfb: httpEntry.ttfb,
|
|
98
100
|
progress: httpEntry.progress,
|
|
99
101
|
};
|
|
100
102
|
} else if (entry.type === 'websocket') {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HttpMethod, NetworkEventSource } from '../../shared/client';
|
|
2
|
+
|
|
3
|
+
export type RequestTypeFilter = 'http' | 'websocket' | 'sse';
|
|
4
|
+
|
|
5
|
+
export type AdvancedFilterState = {
|
|
6
|
+
methods: Set<HttpMethod>;
|
|
7
|
+
sources: Set<NetworkEventSource>;
|
|
8
|
+
status: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
contentType: string;
|
|
11
|
+
failedOnly: boolean;
|
|
12
|
+
inFlightOnly: boolean;
|
|
13
|
+
overriddenOnly: boolean;
|
|
14
|
+
minSize: string;
|
|
15
|
+
maxSize: string;
|
|
16
|
+
minDuration: string;
|
|
17
|
+
maxDuration: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FilterState = {
|
|
21
|
+
text: string;
|
|
22
|
+
types: Set<RequestTypeFilter>;
|
|
23
|
+
advanced: AdvancedFilterState;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_REQUEST_TYPES: RequestTypeFilter[] = [
|
|
27
|
+
'http',
|
|
28
|
+
'websocket',
|
|
29
|
+
'sse',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export const createDefaultFilter = (): FilterState => ({
|
|
33
|
+
text: '',
|
|
34
|
+
types: new Set(DEFAULT_REQUEST_TYPES),
|
|
35
|
+
advanced: {
|
|
36
|
+
methods: new Set(),
|
|
37
|
+
sources: new Set(),
|
|
38
|
+
status: '',
|
|
39
|
+
domain: '',
|
|
40
|
+
contentType: '',
|
|
41
|
+
failedOnly: false,
|
|
42
|
+
inFlightOnly: false,
|
|
43
|
+
overriddenOnly: false,
|
|
44
|
+
minSize: '',
|
|
45
|
+
maxSize: '',
|
|
46
|
+
minDuration: '',
|
|
47
|
+
maxDuration: '',
|
|
48
|
+
},
|
|
49
|
+
});
|
package/src/ui/state/hooks.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { NetworkActivityState } from './store';
|
|
|
4
4
|
import { getProcessedRequests, getSelectedRequest } from './derived';
|
|
5
5
|
|
|
6
6
|
export const useNetworkActivityStore = <T>(
|
|
7
|
-
selector: (state: NetworkActivityState) => T
|
|
7
|
+
selector: (state: NetworkActivityState) => T,
|
|
8
8
|
): T => {
|
|
9
9
|
return useStore(store, selector);
|
|
10
10
|
};
|
|
@@ -39,7 +39,7 @@ export const useNetworkActivityClientManagement = () => {
|
|
|
39
39
|
|
|
40
40
|
export const useWebSocketMessages = (requestId: string) => {
|
|
41
41
|
return useNetworkActivityStore(
|
|
42
|
-
(state) => state.websocketMessages.get(requestId) || []
|
|
42
|
+
(state) => state.websocketMessages.get(requestId) || [],
|
|
43
43
|
);
|
|
44
44
|
};
|
|
45
45
|
|
package/src/ui/state/model.ts
CHANGED
package/src/ui/state/store.ts
CHANGED
|
@@ -26,6 +26,10 @@ const MAX_SSE_MESSAGES_PER_CONNECTION = 32;
|
|
|
26
26
|
|
|
27
27
|
const STORE_VERSION = 1;
|
|
28
28
|
|
|
29
|
+
const getElapsedDuration = (endTimestamp: number, startTimestamp: number) => {
|
|
30
|
+
return Math.max(endTimestamp - startTimestamp, 0);
|
|
31
|
+
};
|
|
32
|
+
|
|
29
33
|
export interface NetworkActivityState {
|
|
30
34
|
// State
|
|
31
35
|
isRecording: boolean;
|
|
@@ -302,6 +306,10 @@ export const createNetworkActivityStore = () =>
|
|
|
302
306
|
const updatedEntry: HttpNetworkEntry = {
|
|
303
307
|
...httpEntry,
|
|
304
308
|
status: 'failed',
|
|
309
|
+
duration: getElapsedDuration(
|
|
310
|
+
eventData.timestamp,
|
|
311
|
+
httpEntry.timestamp,
|
|
312
|
+
),
|
|
305
313
|
error: eventData.error,
|
|
306
314
|
};
|
|
307
315
|
|
|
@@ -414,7 +422,10 @@ export const createNetworkActivityStore = () =>
|
|
|
414
422
|
status: 'closed',
|
|
415
423
|
closeCode: eventData.code,
|
|
416
424
|
closeReason: eventData.reason,
|
|
417
|
-
duration:
|
|
425
|
+
duration: getElapsedDuration(
|
|
426
|
+
eventData.timestamp,
|
|
427
|
+
wsEntry.timestamp,
|
|
428
|
+
),
|
|
418
429
|
};
|
|
419
430
|
|
|
420
431
|
const newEntries = new Map(state.networkEntries);
|
|
@@ -495,6 +506,10 @@ export const createNetworkActivityStore = () =>
|
|
|
495
506
|
const updatedEntry: WebSocketNetworkEntry = {
|
|
496
507
|
...wsEntry,
|
|
497
508
|
status: 'error',
|
|
509
|
+
duration: getElapsedDuration(
|
|
510
|
+
eventData.timestamp,
|
|
511
|
+
wsEntry.timestamp,
|
|
512
|
+
),
|
|
498
513
|
error: eventData.error,
|
|
499
514
|
};
|
|
500
515
|
|
|
@@ -591,6 +606,10 @@ export const createNetworkActivityStore = () =>
|
|
|
591
606
|
const updatedEntry: SSENetworkEntry = {
|
|
592
607
|
...sseEntry,
|
|
593
608
|
status: 'error',
|
|
609
|
+
duration: getElapsedDuration(
|
|
610
|
+
eventData.timestamp,
|
|
611
|
+
sseEntry.timestamp,
|
|
612
|
+
),
|
|
594
613
|
error: eventData.error.message,
|
|
595
614
|
};
|
|
596
615
|
|
|
@@ -611,7 +630,10 @@ export const createNetworkActivityStore = () =>
|
|
|
611
630
|
const updatedEntry: SSENetworkEntry = {
|
|
612
631
|
...sseEntry,
|
|
613
632
|
status: 'closed',
|
|
614
|
-
duration:
|
|
633
|
+
duration: getElapsedDuration(
|
|
634
|
+
eventData.timestamp,
|
|
635
|
+
sseEntry.timestamp,
|
|
636
|
+
),
|
|
615
637
|
};
|
|
616
638
|
|
|
617
639
|
const newEntries = new Map(state.networkEntries);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createDefaultFilter } from '../../state/filter';
|
|
3
|
+
import type { FilterState } from '../../state/filter';
|
|
4
|
+
import type { ProcessedRequest } from '../../state/model';
|
|
5
|
+
import { matchesRequestFilter } from '../requestFilters';
|
|
6
|
+
|
|
7
|
+
const allTypesFilter = (text: string): FilterState => ({
|
|
8
|
+
...createDefaultFilter(),
|
|
9
|
+
text,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const request: ProcessedRequest = {
|
|
13
|
+
id: 'request-1',
|
|
14
|
+
type: 'http',
|
|
15
|
+
name: 'https://example.com/users',
|
|
16
|
+
status: 'finished',
|
|
17
|
+
timestamp: 0,
|
|
18
|
+
duration: 100,
|
|
19
|
+
size: null,
|
|
20
|
+
method: 'GET',
|
|
21
|
+
httpStatus: 404,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('matchesRequestFilter', () => {
|
|
25
|
+
it('matches HTTP status codes', () => {
|
|
26
|
+
expect(matchesRequestFilter(request, allTypesFilter('404'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('ignores whitespace-only text filters', () => {
|
|
30
|
+
expect(matchesRequestFilter(request, allTypesFilter(' '))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
HttpNetworkEntry,
|
|
4
|
+
NetworkEntry,
|
|
5
|
+
RequestId,
|
|
6
|
+
SSENetworkEntry,
|
|
7
|
+
WebSocketMessage,
|
|
8
|
+
WebSocketNetworkEntry,
|
|
9
|
+
} from '../../state/model';
|
|
10
|
+
import {
|
|
11
|
+
createNetworkActivitySessionExport,
|
|
12
|
+
getNetworkActivitySessionExportFileName,
|
|
13
|
+
} from '../sessionExport';
|
|
14
|
+
|
|
15
|
+
const httpEntry: HttpNetworkEntry = {
|
|
16
|
+
id: 'request-1',
|
|
17
|
+
type: 'http',
|
|
18
|
+
timestamp: 100,
|
|
19
|
+
duration: 50,
|
|
20
|
+
source: 'builtin',
|
|
21
|
+
request: {
|
|
22
|
+
url: 'https://example.com/api',
|
|
23
|
+
method: 'GET',
|
|
24
|
+
headers: {
|
|
25
|
+
accept: 'application/json',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
response: {
|
|
29
|
+
url: 'https://example.com/api',
|
|
30
|
+
status: 200,
|
|
31
|
+
statusText: 'OK',
|
|
32
|
+
headers: {
|
|
33
|
+
'content-type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
contentType: 'application/json',
|
|
36
|
+
size: 17,
|
|
37
|
+
responseTime: 150,
|
|
38
|
+
body: {
|
|
39
|
+
type: 'application/json',
|
|
40
|
+
data: '{"ok":true}',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
status: 'finished',
|
|
44
|
+
ttfb: 20,
|
|
45
|
+
size: 17,
|
|
46
|
+
resourceType: 'Fetch',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const websocketEntry: WebSocketNetworkEntry = {
|
|
50
|
+
id: 'ws-socket-1',
|
|
51
|
+
type: 'websocket',
|
|
52
|
+
timestamp: 200,
|
|
53
|
+
duration: 100,
|
|
54
|
+
source: 'builtin',
|
|
55
|
+
connection: {
|
|
56
|
+
url: 'wss://example.com/socket',
|
|
57
|
+
socketId: 'socket-1',
|
|
58
|
+
protocols: ['chat'],
|
|
59
|
+
options: [],
|
|
60
|
+
},
|
|
61
|
+
status: 'closed',
|
|
62
|
+
closeCode: 1000,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const sseEntry: SSENetworkEntry = {
|
|
66
|
+
id: 'request-sse',
|
|
67
|
+
type: 'sse',
|
|
68
|
+
timestamp: 300,
|
|
69
|
+
duration: 200,
|
|
70
|
+
source: 'builtin',
|
|
71
|
+
request: {
|
|
72
|
+
url: 'https://example.com/events',
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: {},
|
|
75
|
+
},
|
|
76
|
+
response: {
|
|
77
|
+
url: 'https://example.com/events',
|
|
78
|
+
status: 200,
|
|
79
|
+
statusText: 'OK',
|
|
80
|
+
headers: {
|
|
81
|
+
'content-type': 'text/event-stream',
|
|
82
|
+
},
|
|
83
|
+
contentType: 'text/event-stream',
|
|
84
|
+
size: 0,
|
|
85
|
+
responseTime: 310,
|
|
86
|
+
},
|
|
87
|
+
status: 'closed',
|
|
88
|
+
messages: [
|
|
89
|
+
{
|
|
90
|
+
id: 'sse-message-1',
|
|
91
|
+
type: 'message',
|
|
92
|
+
data: 'hello',
|
|
93
|
+
timestamp: 320,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const websocketMessages: WebSocketMessage[] = [
|
|
99
|
+
{
|
|
100
|
+
id: 'websocket-message-1',
|
|
101
|
+
direction: 'sent',
|
|
102
|
+
data: 'ping',
|
|
103
|
+
messageType: 'text',
|
|
104
|
+
timestamp: 210,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'websocket-message-2',
|
|
108
|
+
direction: 'received',
|
|
109
|
+
data: 'pong',
|
|
110
|
+
messageType: 'text',
|
|
111
|
+
timestamp: 220,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
describe('sessionExport', () => {
|
|
116
|
+
it('exports captured HTTP and realtime session entries', () => {
|
|
117
|
+
const networkEntries = new Map<RequestId, NetworkEntry>([
|
|
118
|
+
[sseEntry.id, sseEntry],
|
|
119
|
+
[httpEntry.id, httpEntry],
|
|
120
|
+
[websocketEntry.id, websocketEntry],
|
|
121
|
+
]);
|
|
122
|
+
const exportData = createNetworkActivitySessionExport(
|
|
123
|
+
networkEntries,
|
|
124
|
+
new Map([[websocketEntry.id, websocketMessages]]),
|
|
125
|
+
new Date('2026-05-14T10:00:00.000Z'),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(exportData).toMatchObject({
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
tool: 'rozenite-network-activity',
|
|
131
|
+
exportedAt: '2026-05-14T10:00:00.000Z',
|
|
132
|
+
summary: {
|
|
133
|
+
totalEntries: 3,
|
|
134
|
+
httpRequests: 1,
|
|
135
|
+
webSocketConnections: 1,
|
|
136
|
+
sseConnections: 1,
|
|
137
|
+
realtimeMessages: 3,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
expect(exportData.entries.map((entry) => entry.id)).toEqual([
|
|
141
|
+
httpEntry.id,
|
|
142
|
+
websocketEntry.id,
|
|
143
|
+
sseEntry.id,
|
|
144
|
+
]);
|
|
145
|
+
expect(exportData.entries[0]).toMatchObject({
|
|
146
|
+
type: 'http',
|
|
147
|
+
request: {
|
|
148
|
+
url: 'https://example.com/api',
|
|
149
|
+
},
|
|
150
|
+
response: {
|
|
151
|
+
status: 200,
|
|
152
|
+
body: {
|
|
153
|
+
data: '{"ok":true}',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
expect(exportData.entries[1]).toMatchObject({
|
|
158
|
+
type: 'websocket',
|
|
159
|
+
messages: websocketMessages,
|
|
160
|
+
});
|
|
161
|
+
expect(exportData.entries[2]).toMatchObject({
|
|
162
|
+
type: 'sse',
|
|
163
|
+
messages: sseEntry.messages,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('creates filesystem-friendly export filenames', () => {
|
|
168
|
+
expect(
|
|
169
|
+
getNetworkActivitySessionExportFileName(
|
|
170
|
+
new Date('2026-05-14T10:00:00.123Z'),
|
|
171
|
+
),
|
|
172
|
+
).toBe('rozenite-network-session-2026-05-14T10-00-00Z.json');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -126,6 +126,79 @@ describe('symbolication', () => {
|
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it('keeps internal Hermes frames out of the Metro symbolication request', async () => {
|
|
130
|
+
const initiator: Initiator = {
|
|
131
|
+
type: 'script',
|
|
132
|
+
generatedUrl: 'address at InternalBytecode.js',
|
|
133
|
+
generatedLineNumber: 1,
|
|
134
|
+
generatedColumnNumber: 12345,
|
|
135
|
+
symbolicationStatus: 'pending',
|
|
136
|
+
stack: [
|
|
137
|
+
{
|
|
138
|
+
functionName: 'anonymous',
|
|
139
|
+
generatedUrl: 'address at InternalBytecode.js',
|
|
140
|
+
generatedLineNumber: 1,
|
|
141
|
+
generatedColumnNumber: 12345,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
functionName: 'loadUsers',
|
|
145
|
+
generatedUrl: 'http://localhost:8081/index.bundle',
|
|
146
|
+
generatedLineNumber: 1,
|
|
147
|
+
generatedColumnNumber: 200,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
const symbolicateStackTrace = vi.fn().mockResolvedValue({
|
|
152
|
+
stack: [
|
|
153
|
+
{
|
|
154
|
+
methodName: 'loadUsers',
|
|
155
|
+
file: 'apps/playground/src/app/api.ts',
|
|
156
|
+
lineNumber: 30,
|
|
157
|
+
column: 6,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const symbolicatedInitiator = await symbolicateInitiator(
|
|
163
|
+
initiator,
|
|
164
|
+
symbolicateStackTrace,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(symbolicateStackTrace).toHaveBeenCalledWith([
|
|
168
|
+
{
|
|
169
|
+
methodName: 'loadUsers',
|
|
170
|
+
file: 'http://localhost:8081/index.bundle',
|
|
171
|
+
lineNumber: 1,
|
|
172
|
+
column: 200,
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
expect(symbolicatedInitiator).toMatchObject({
|
|
176
|
+
type: 'script',
|
|
177
|
+
functionName: 'loadUsers',
|
|
178
|
+
url: 'apps/playground/src/app/api.ts',
|
|
179
|
+
lineNumber: 30,
|
|
180
|
+
columnNumber: 6,
|
|
181
|
+
symbolicationStatus: 'complete',
|
|
182
|
+
stack: [
|
|
183
|
+
{
|
|
184
|
+
functionName: 'anonymous',
|
|
185
|
+
generatedUrl: 'address at InternalBytecode.js',
|
|
186
|
+
generatedLineNumber: 1,
|
|
187
|
+
generatedColumnNumber: 12345,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
functionName: 'loadUsers',
|
|
191
|
+
url: 'apps/playground/src/app/api.ts',
|
|
192
|
+
lineNumber: 30,
|
|
193
|
+
columnNumber: 6,
|
|
194
|
+
generatedUrl: 'http://localhost:8081/index.bundle',
|
|
195
|
+
generatedLineNumber: 1,
|
|
196
|
+
generatedColumnNumber: 200,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
129
202
|
it('reports symbolication failures on the initiator', async () => {
|
|
130
203
|
const initiator: Initiator = {
|
|
131
204
|
type: 'script',
|