@rozenite/network-activity-plugin 1.9.0 → 1.11.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 +60 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-hSoryVpJ.js → App-CEESZAW_.js} +7520 -937
- package/dist/devtools/assets/{App-m6xge0az.css → App-xppYUJvX.css} +246 -2
- package/dist/react-native/chunks/boot-recording.cjs +138 -14
- package/dist/react-native/chunks/boot-recording.js +138 -14
- package/dist/react-native/chunks/get-nitro-module.cjs +4 -1
- package/dist/react-native/chunks/get-nitro-module.js +4 -1
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +20 -1
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +20 -1
- package/dist/react-native/index.d.ts +37 -1
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +37 -1
- package/package.json +12 -7
- package/src/react-native/agent/use-network-activity-agent-tools.ts +22 -4
- package/src/react-native/http/__tests__/http-utils.test.ts +228 -0
- package/src/react-native/http/http-utils.ts +208 -25
- package/src/react-native/network-inspector.ts +2 -2
- package/src/react-native/nitro-fetch/get-nitro-module.ts +5 -1
- package/src/react-native/nitro-fetch/nitro-network-inspector.ts +8 -2
- package/src/shared/http-events.ts +40 -1
- package/src/ui/components/CodeBlock.tsx +45 -1
- package/src/ui/components/FilterBar.tsx +337 -61
- package/src/ui/components/HexView.tsx +54 -0
- package/src/ui/components/MetadataCard.tsx +95 -0
- package/src/ui/components/NetworkTimeline.tsx +422 -0
- package/src/ui/components/RequestList.tsx +19 -40
- package/src/ui/components/SidePanel.tsx +42 -1
- package/src/ui/components/Toolbar.tsx +13 -1
- package/src/ui/components/ViewToggle.tsx +44 -0
- package/src/ui/components/XmlTree.tsx +160 -0
- package/src/ui/components/__tests__/CodeBlock.test.tsx +89 -0
- package/src/ui/components/__tests__/HexView.test.tsx +41 -0
- package/src/ui/components/__tests__/MetadataCard.test.tsx +107 -0
- package/src/ui/components/__tests__/ViewToggle.test.tsx +80 -0
- package/src/ui/components/__tests__/XmlTree.test.tsx +149 -0
- package/src/ui/hooks/useNetworkActivitySessionExport.ts +39 -0
- package/src/ui/response-renderers/__tests__/binary-too-large.test.tsx +56 -0
- package/src/ui/response-renderers/__tests__/binary.test.tsx +96 -0
- package/src/ui/response-renderers/__tests__/dispatch.test.ts +124 -0
- package/src/ui/response-renderers/__tests__/html.test.tsx +101 -0
- package/src/ui/response-renderers/__tests__/image.test.tsx +73 -0
- package/src/ui/response-renderers/__tests__/json.test.tsx +95 -0
- package/src/ui/response-renderers/__tests__/svg.test.tsx +46 -0
- package/src/ui/response-renderers/__tests__/xml.test.tsx +100 -0
- package/src/ui/response-renderers/binary-too-large.tsx +36 -0
- package/src/ui/response-renderers/binary.tsx +31 -0
- package/src/ui/response-renderers/empty.tsx +14 -0
- package/src/ui/response-renderers/html.tsx +36 -0
- package/src/ui/response-renderers/image.tsx +37 -0
- package/src/ui/response-renderers/index.ts +55 -0
- package/src/ui/response-renderers/json.tsx +40 -0
- package/src/ui/response-renderers/svg.tsx +27 -0
- package/src/ui/response-renderers/text-fallback.tsx +14 -0
- package/src/ui/response-renderers/types.ts +38 -0
- package/src/ui/response-renderers/unknown.tsx +18 -0
- package/src/ui/response-renderers/xml.tsx +46 -0
- package/src/ui/state/__tests__/store.test.ts +77 -0
- package/src/ui/state/derived.ts +14 -0
- package/src/ui/state/filter.ts +49 -0
- package/src/ui/state/hooks.ts +2 -2
- package/src/ui/state/model.ts +7 -1
- package/src/ui/state/store.ts +63 -4
- package/src/ui/tabs/InitiatorTab.tsx +230 -0
- package/src/ui/tabs/ResponseTab.tsx +80 -97
- package/src/ui/tabs/__tests__/ResponseTab.test.tsx +102 -0
- package/src/ui/utils/__tests__/download.test.ts +115 -0
- package/src/ui/utils/__tests__/hex.test.ts +84 -0
- 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 +207 -0
- package/src/ui/utils/__tests__/timelineModel.test.ts +170 -0
- package/src/ui/utils/download.ts +161 -0
- package/src/ui/utils/hex.ts +59 -0
- package/src/ui/utils/initiator.ts +136 -0
- package/src/ui/utils/requestFilters.ts +183 -0
- package/src/ui/utils/sessionExport.ts +185 -0
- package/src/ui/utils/symbolication.ts +248 -0
- package/src/ui/utils/timelineModel.ts +352 -0
- package/src/ui/views/InspectorView.tsx +43 -8
- package/src/utils/__tests__/getContentTypeMimeType.test.ts +34 -0
- package/src/utils/getContentTypeMimeType.ts +14 -0
- package/vite.config.ts +5 -1
- package/vitest.setup.ts +31 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Byte-level helpers shared by HexView and any future binary-inspection
|
|
2
|
+
// surface. Kept small and pure so unit tests don't need a DOM.
|
|
3
|
+
|
|
4
|
+
export const BYTES_PER_HEX_ROW = 16;
|
|
5
|
+
// One blank space between bytes, plus an extra blank space after byte 8
|
|
6
|
+
// to group the row into halves. Same convention as `xxd`.
|
|
7
|
+
export const BYTES_PER_GROUP = 8;
|
|
8
|
+
|
|
9
|
+
const ASCII_PRINTABLE_MIN = 0x20;
|
|
10
|
+
const ASCII_PRINTABLE_MAX = 0x7e;
|
|
11
|
+
|
|
12
|
+
export const toHexPair = (byte: number): string =>
|
|
13
|
+
byte.toString(16).toUpperCase().padStart(2, '0');
|
|
14
|
+
|
|
15
|
+
export const toAsciiChar = (byte: number): string =>
|
|
16
|
+
byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX
|
|
17
|
+
? String.fromCharCode(byte)
|
|
18
|
+
: '.';
|
|
19
|
+
|
|
20
|
+
export type HexRow = {
|
|
21
|
+
offset: string;
|
|
22
|
+
hex: string;
|
|
23
|
+
ascii: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sliceHexBytes = (
|
|
27
|
+
bytes: Uint8Array,
|
|
28
|
+
start: number,
|
|
29
|
+
end: number,
|
|
30
|
+
): number[] => {
|
|
31
|
+
const out: number[] = [];
|
|
32
|
+
const limit = Math.min(end, bytes.length);
|
|
33
|
+
for (let i = start; i < limit; i++) {
|
|
34
|
+
out.push(bytes[i]);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const formatHexRow = (bytes: Uint8Array, rowStart: number): HexRow => {
|
|
40
|
+
const rowEnd = rowStart + BYTES_PER_HEX_ROW;
|
|
41
|
+
const leftSlice = sliceHexBytes(bytes, rowStart, rowStart + BYTES_PER_GROUP);
|
|
42
|
+
const rightSlice = sliceHexBytes(bytes, rowStart + BYTES_PER_GROUP, rowEnd);
|
|
43
|
+
|
|
44
|
+
const left = leftSlice.map(toHexPair).join(' ');
|
|
45
|
+
const right = rightSlice.map(toHexPair).join(' ');
|
|
46
|
+
const hex = right ? `${left} ${right}` : left;
|
|
47
|
+
|
|
48
|
+
const asciiSlice = sliceHexBytes(bytes, rowStart, rowEnd);
|
|
49
|
+
const ascii = asciiSlice.map(toAsciiChar).join('');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
offset: rowStart.toString(16).padStart(8, '0'),
|
|
53
|
+
hex,
|
|
54
|
+
ascii,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const rowCountForByteLength = (byteLength: number): number =>
|
|
59
|
+
Math.ceil(byteLength / BYTES_PER_HEX_ROW);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Initiator, InitiatorStackFrame } from '../../shared/client';
|
|
2
|
+
|
|
3
|
+
type FrameLocation = {
|
|
4
|
+
functionName?: string;
|
|
5
|
+
url?: string;
|
|
6
|
+
lineNumber?: number;
|
|
7
|
+
columnNumber?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SOURCE_PATH_PATTERN = /(?:^|\/)((?:apps|packages|src)\/.+)$/;
|
|
11
|
+
|
|
12
|
+
const safeDecodeURIComponent = (value: string) => {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getGeneratedFrameLocation = (
|
|
21
|
+
frame?: InitiatorStackFrame | Initiator,
|
|
22
|
+
): FrameLocation | null => {
|
|
23
|
+
if (!frame?.generatedUrl) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
functionName: frame.functionName,
|
|
29
|
+
url: frame.generatedUrl,
|
|
30
|
+
lineNumber: frame.generatedLineNumber,
|
|
31
|
+
columnNumber: frame.generatedColumnNumber,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getSourceFrameLocation = (
|
|
36
|
+
frame?: InitiatorStackFrame | Initiator,
|
|
37
|
+
): FrameLocation | null => {
|
|
38
|
+
if (!frame?.url) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
functionName: frame.functionName,
|
|
44
|
+
url: frame.url,
|
|
45
|
+
lineNumber: frame.lineNumber,
|
|
46
|
+
columnNumber: frame.columnNumber,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const formatSourcePath = (url: string) => {
|
|
51
|
+
const withoutQuery = url.split(/[?#]/)[0];
|
|
52
|
+
const decodedPath = safeDecodeURIComponent(withoutQuery).replace(
|
|
53
|
+
/^file:\/\//,
|
|
54
|
+
'',
|
|
55
|
+
);
|
|
56
|
+
const bundlePathMatch = decodedPath.match(/([^/]+\.bundle)(?:\/|$)/);
|
|
57
|
+
|
|
58
|
+
if (bundlePathMatch) {
|
|
59
|
+
return bundlePathMatch[1];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sourcePathMatch = decodedPath.match(SOURCE_PATH_PATTERN);
|
|
63
|
+
|
|
64
|
+
if (sourcePathMatch) {
|
|
65
|
+
return sourcePathMatch[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const parsedUrl = new URL(url);
|
|
70
|
+
const fileName = parsedUrl.pathname.split('/').filter(Boolean).pop();
|
|
71
|
+
|
|
72
|
+
return fileName || parsedUrl.hostname || url;
|
|
73
|
+
} catch {
|
|
74
|
+
const pathParts = decodedPath.split('/').filter(Boolean);
|
|
75
|
+
|
|
76
|
+
return pathParts.slice(-3).join('/') || decodedPath || url;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const formatFrameLocation = (frame?: FrameLocation | null) => {
|
|
81
|
+
if (!frame?.url) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const locationParts = [formatSourcePath(frame.url)];
|
|
86
|
+
if (frame.lineNumber !== undefined) {
|
|
87
|
+
locationParts.push(String(frame.lineNumber));
|
|
88
|
+
}
|
|
89
|
+
if (frame.columnNumber !== undefined) {
|
|
90
|
+
locationParts.push(String(frame.columnNumber));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return locationParts.join(':');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const getBestInitiatorFrame = (
|
|
97
|
+
initiator?: Initiator,
|
|
98
|
+
): FrameLocation | null => {
|
|
99
|
+
const directSourceFrame = getSourceFrameLocation(initiator);
|
|
100
|
+
if (directSourceFrame) {
|
|
101
|
+
return directSourceFrame;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stackSourceFrame =
|
|
105
|
+
initiator?.stack
|
|
106
|
+
?.filter((frame) => !frame.isCollapsed)
|
|
107
|
+
.map(getSourceFrameLocation)
|
|
108
|
+
.find((frame): frame is FrameLocation => frame !== null) ?? null;
|
|
109
|
+
|
|
110
|
+
return stackSourceFrame ?? getGeneratedFrameLocation(initiator);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getInitiatorLabel = (initiator?: Initiator) => {
|
|
114
|
+
if (!initiator) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (initiator.symbolicationStatus === 'pending') {
|
|
119
|
+
return 'Resolving source...';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const bestFrame = getBestInitiatorFrame(initiator);
|
|
123
|
+
if (!bestFrame) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
bestFrame.functionName ??
|
|
129
|
+
formatFrameLocation(bestFrame) ??
|
|
130
|
+
(bestFrame.url ? formatSourcePath(bestFrame.url) : null)
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const getInitiatorLocationLabel = (initiator?: Initiator) => {
|
|
135
|
+
return formatFrameLocation(getBestInitiatorFrame(initiator));
|
|
136
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { FilterState } from '../state/filter';
|
|
2
|
+
import type { ProcessedRequest } from '../state/model';
|
|
3
|
+
import type { HttpMethod } from '../../shared/client';
|
|
4
|
+
|
|
5
|
+
export type RequestFilterOptions = {
|
|
6
|
+
hasOverride?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const parseThreshold = (value: string): number | null => {
|
|
10
|
+
const normalizedValue = value.trim();
|
|
11
|
+
if (!normalizedValue) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsedValue = Number(normalizedValue);
|
|
16
|
+
return Number.isFinite(parsedValue) ? parsedValue : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const matchesStatusFilter = (
|
|
20
|
+
statusCode: number | undefined,
|
|
21
|
+
statusFilter: string,
|
|
22
|
+
) => {
|
|
23
|
+
const normalizedFilter = statusFilter.trim().toLowerCase();
|
|
24
|
+
if (!normalizedFilter) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (statusCode === undefined) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const statusRangeMatch = normalizedFilter.match(/^(\d{3})\s*-\s*(\d{3})$/);
|
|
33
|
+
if (statusRangeMatch) {
|
|
34
|
+
const min = Number(statusRangeMatch[1]);
|
|
35
|
+
const max = Number(statusRangeMatch[2]);
|
|
36
|
+
return statusCode >= min && statusCode <= max;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const statusClassMatch = normalizedFilter.match(/^([1-5])xx$/);
|
|
40
|
+
if (statusClassMatch) {
|
|
41
|
+
return Math.floor(statusCode / 100) === Number(statusClassMatch[1]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const comparisonMatch = normalizedFilter.match(/^(>=|<=|>|<)\s*(\d{3})$/);
|
|
45
|
+
if (comparisonMatch) {
|
|
46
|
+
const value = Number(comparisonMatch[2]);
|
|
47
|
+
switch (comparisonMatch[1]) {
|
|
48
|
+
case '>=':
|
|
49
|
+
return statusCode >= value;
|
|
50
|
+
case '<=':
|
|
51
|
+
return statusCode <= value;
|
|
52
|
+
case '>':
|
|
53
|
+
return statusCode > value;
|
|
54
|
+
case '<':
|
|
55
|
+
return statusCode < value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return statusCode === Number(normalizedFilter);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isInFlightStatus = (status: ProcessedRequest['status']) => {
|
|
63
|
+
return ['pending', 'loading', 'connecting', 'open'].includes(status);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const isFailedStatus = (status: ProcessedRequest['status']) => {
|
|
67
|
+
return ['failed', 'error'].includes(status);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const isHttpMethod = (
|
|
71
|
+
method: ProcessedRequest['method'],
|
|
72
|
+
): method is HttpMethod => method !== 'WS' && method !== 'SSE';
|
|
73
|
+
|
|
74
|
+
const extractDomainAndPath = (url: string) => {
|
|
75
|
+
try {
|
|
76
|
+
const { hostname, pathname, search, hash, port } = new URL(url);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
domain: `${hostname}${port ? `:${port}` : ''}`,
|
|
80
|
+
path: `${pathname}${search}${hash}`,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return { domain: 'unknown', path: url };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const matchesRequestFilter = (
|
|
88
|
+
request: ProcessedRequest,
|
|
89
|
+
filter: FilterState,
|
|
90
|
+
options: RequestFilterOptions = {},
|
|
91
|
+
) => {
|
|
92
|
+
if (filter.types.size > 0 && !filter.types.has(request.type)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
filter.advanced.methods.size > 0 &&
|
|
98
|
+
(!isHttpMethod(request.method) ||
|
|
99
|
+
!filter.advanced.methods.has(request.method))
|
|
100
|
+
) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
filter.advanced.sources.size > 0 &&
|
|
106
|
+
(!request.source || !filter.advanced.sources.has(request.source))
|
|
107
|
+
) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!matchesStatusFilter(request.httpStatus, filter.advanced.status)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { domain, path } = extractDomainAndPath(request.name);
|
|
116
|
+
const domainFilter = filter.advanced.domain.trim().toLowerCase();
|
|
117
|
+
if (domainFilter && !domain.toLowerCase().includes(domainFilter)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const contentTypeFilter = filter.advanced.contentType.trim().toLowerCase();
|
|
122
|
+
if (
|
|
123
|
+
contentTypeFilter &&
|
|
124
|
+
!request.contentType?.toLowerCase().includes(contentTypeFilter)
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (filter.advanced.failedOnly && !isFailedStatus(request.status)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (filter.advanced.inFlightOnly && !isInFlightStatus(request.status)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (filter.advanced.overriddenOnly && !options.hasOverride) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const minSize = parseThreshold(filter.advanced.minSize);
|
|
142
|
+
if (minSize !== null && (request.size === null || request.size < minSize)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const maxSize = parseThreshold(filter.advanced.maxSize);
|
|
147
|
+
if (maxSize !== null && (request.size === null || request.size > maxSize)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const duration = request.duration || 0;
|
|
152
|
+
const minDuration = parseThreshold(filter.advanced.minDuration);
|
|
153
|
+
if (minDuration !== null && duration < minDuration) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const maxDuration = parseThreshold(filter.advanced.maxDuration);
|
|
158
|
+
if (maxDuration !== null && duration > maxDuration) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const searchText = filter.text.trim().toLowerCase();
|
|
163
|
+
if (!searchText) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const searchableFields = [
|
|
168
|
+
request.name,
|
|
169
|
+
request.method,
|
|
170
|
+
request.status,
|
|
171
|
+
request.httpStatus,
|
|
172
|
+
request.source,
|
|
173
|
+
request.type,
|
|
174
|
+
request.contentType,
|
|
175
|
+
domain,
|
|
176
|
+
path,
|
|
177
|
+
]
|
|
178
|
+
.filter((value) => value !== undefined && value !== null)
|
|
179
|
+
.join(' ')
|
|
180
|
+
.toLowerCase();
|
|
181
|
+
|
|
182
|
+
return searchableFields.includes(searchText);
|
|
183
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpNetworkEntry,
|
|
3
|
+
NetworkEntry,
|
|
4
|
+
RequestId,
|
|
5
|
+
SSENetworkEntry,
|
|
6
|
+
WebSocketMessage,
|
|
7
|
+
WebSocketNetworkEntry,
|
|
8
|
+
} from '../state/model';
|
|
9
|
+
|
|
10
|
+
const EXPORT_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
type ExportedHttpEntry = {
|
|
13
|
+
id: RequestId;
|
|
14
|
+
type: 'http';
|
|
15
|
+
source?: NetworkEntry['source'];
|
|
16
|
+
timestamp: number;
|
|
17
|
+
duration: number | null;
|
|
18
|
+
status: HttpNetworkEntry['status'];
|
|
19
|
+
error?: string;
|
|
20
|
+
canceled?: boolean;
|
|
21
|
+
request: HttpNetworkEntry['request'];
|
|
22
|
+
response: HttpNetworkEntry['response'] | null;
|
|
23
|
+
size: number | null;
|
|
24
|
+
ttfb: number | null;
|
|
25
|
+
initiator?: HttpNetworkEntry['initiator'];
|
|
26
|
+
resourceType?: HttpNetworkEntry['resourceType'];
|
|
27
|
+
progress?: HttpNetworkEntry['progress'];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ExportedWebSocketEntry = {
|
|
31
|
+
id: RequestId;
|
|
32
|
+
type: 'websocket';
|
|
33
|
+
source?: NetworkEntry['source'];
|
|
34
|
+
timestamp: number;
|
|
35
|
+
duration: number | null;
|
|
36
|
+
status: WebSocketNetworkEntry['status'];
|
|
37
|
+
connection: WebSocketNetworkEntry['connection'];
|
|
38
|
+
error?: string;
|
|
39
|
+
closeCode?: number;
|
|
40
|
+
closeReason?: string;
|
|
41
|
+
messages: WebSocketMessage[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ExportedSSEEntry = {
|
|
45
|
+
id: RequestId;
|
|
46
|
+
type: 'sse';
|
|
47
|
+
source?: NetworkEntry['source'];
|
|
48
|
+
timestamp: number;
|
|
49
|
+
duration: number | null;
|
|
50
|
+
status: SSENetworkEntry['status'];
|
|
51
|
+
error?: string;
|
|
52
|
+
request: SSENetworkEntry['request'];
|
|
53
|
+
response: SSENetworkEntry['response'] | null;
|
|
54
|
+
initiator?: SSENetworkEntry['initiator'];
|
|
55
|
+
resourceType?: SSENetworkEntry['resourceType'];
|
|
56
|
+
messages: SSENetworkEntry['messages'];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ExportedNetworkEntry =
|
|
60
|
+
| ExportedHttpEntry
|
|
61
|
+
| ExportedWebSocketEntry
|
|
62
|
+
| ExportedSSEEntry;
|
|
63
|
+
|
|
64
|
+
export type NetworkActivitySessionExport = {
|
|
65
|
+
schemaVersion: typeof EXPORT_SCHEMA_VERSION;
|
|
66
|
+
tool: 'rozenite-network-activity';
|
|
67
|
+
exportedAt: string;
|
|
68
|
+
summary: {
|
|
69
|
+
totalEntries: number;
|
|
70
|
+
httpRequests: number;
|
|
71
|
+
webSocketConnections: number;
|
|
72
|
+
sseConnections: number;
|
|
73
|
+
realtimeMessages: number;
|
|
74
|
+
};
|
|
75
|
+
entries: ExportedNetworkEntry[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getDuration = (duration: number | undefined) => duration ?? null;
|
|
79
|
+
|
|
80
|
+
const serializeHttpEntry = (entry: HttpNetworkEntry): ExportedHttpEntry => ({
|
|
81
|
+
id: entry.id,
|
|
82
|
+
type: 'http',
|
|
83
|
+
source: entry.source,
|
|
84
|
+
timestamp: entry.timestamp,
|
|
85
|
+
duration: getDuration(entry.duration),
|
|
86
|
+
status: entry.status,
|
|
87
|
+
error: entry.error,
|
|
88
|
+
canceled: entry.canceled,
|
|
89
|
+
request: entry.request,
|
|
90
|
+
response: entry.response ?? null,
|
|
91
|
+
size: entry.size ?? null,
|
|
92
|
+
ttfb: entry.ttfb ?? null,
|
|
93
|
+
initiator: entry.initiator,
|
|
94
|
+
resourceType: entry.resourceType,
|
|
95
|
+
progress: entry.progress,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const serializeWebSocketEntry = (
|
|
99
|
+
entry: WebSocketNetworkEntry,
|
|
100
|
+
websocketMessages: Map<RequestId, WebSocketMessage[]>,
|
|
101
|
+
): ExportedWebSocketEntry => ({
|
|
102
|
+
id: entry.id,
|
|
103
|
+
type: 'websocket',
|
|
104
|
+
source: entry.source,
|
|
105
|
+
timestamp: entry.timestamp,
|
|
106
|
+
duration: getDuration(entry.duration),
|
|
107
|
+
status: entry.status,
|
|
108
|
+
connection: entry.connection,
|
|
109
|
+
error: entry.error,
|
|
110
|
+
closeCode: entry.closeCode,
|
|
111
|
+
closeReason: entry.closeReason,
|
|
112
|
+
messages: websocketMessages.get(entry.id) ?? [],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const serializeSSEEntry = (entry: SSENetworkEntry): ExportedSSEEntry => ({
|
|
116
|
+
id: entry.id,
|
|
117
|
+
type: 'sse',
|
|
118
|
+
source: entry.source,
|
|
119
|
+
timestamp: entry.timestamp,
|
|
120
|
+
duration: getDuration(entry.duration),
|
|
121
|
+
status: entry.status,
|
|
122
|
+
error: entry.error,
|
|
123
|
+
request: entry.request,
|
|
124
|
+
response: entry.response ?? null,
|
|
125
|
+
initiator: entry.initiator,
|
|
126
|
+
resourceType: entry.resourceType,
|
|
127
|
+
messages: entry.messages,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const serializeEntry = (
|
|
131
|
+
entry: NetworkEntry,
|
|
132
|
+
websocketMessages: Map<RequestId, WebSocketMessage[]>,
|
|
133
|
+
): ExportedNetworkEntry => {
|
|
134
|
+
switch (entry.type) {
|
|
135
|
+
case 'http':
|
|
136
|
+
return serializeHttpEntry(entry);
|
|
137
|
+
case 'websocket':
|
|
138
|
+
return serializeWebSocketEntry(entry, websocketMessages);
|
|
139
|
+
case 'sse':
|
|
140
|
+
return serializeSSEEntry(entry);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const createNetworkActivitySessionExport = (
|
|
145
|
+
networkEntries: Map<RequestId, NetworkEntry>,
|
|
146
|
+
websocketMessages: Map<RequestId, WebSocketMessage[]>,
|
|
147
|
+
exportedAt = new Date(),
|
|
148
|
+
): NetworkActivitySessionExport => {
|
|
149
|
+
const entries = Array.from(networkEntries.values())
|
|
150
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
151
|
+
.map((entry) => serializeEntry(entry, websocketMessages));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
schemaVersion: EXPORT_SCHEMA_VERSION,
|
|
155
|
+
tool: 'rozenite-network-activity',
|
|
156
|
+
exportedAt: exportedAt.toISOString(),
|
|
157
|
+
summary: {
|
|
158
|
+
totalEntries: entries.length,
|
|
159
|
+
httpRequests: entries.filter((entry) => entry.type === 'http').length,
|
|
160
|
+
webSocketConnections: entries.filter(
|
|
161
|
+
(entry) => entry.type === 'websocket',
|
|
162
|
+
).length,
|
|
163
|
+
sseConnections: entries.filter((entry) => entry.type === 'sse').length,
|
|
164
|
+
realtimeMessages: entries.reduce((count, entry) => {
|
|
165
|
+
if (entry.type === 'websocket' || entry.type === 'sse') {
|
|
166
|
+
return count + entry.messages.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return count;
|
|
170
|
+
}, 0),
|
|
171
|
+
},
|
|
172
|
+
entries,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const getNetworkActivitySessionExportFileName = (
|
|
177
|
+
exportedAt = new Date(),
|
|
178
|
+
) => {
|
|
179
|
+
const timestamp = exportedAt
|
|
180
|
+
.toISOString()
|
|
181
|
+
.replace(/\.\d{3}Z$/, 'Z')
|
|
182
|
+
.replace(/[:]/g, '-');
|
|
183
|
+
|
|
184
|
+
return `rozenite-network-session-${timestamp}.json`;
|
|
185
|
+
};
|