@rozenite/network-activity-plugin 1.8.1 → 1.10.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 +56 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-m6xge0az.css → App-CUXU0mup.css} +152 -2
- package/dist/devtools/assets/{App-B3xlUjs6.js → App-DsimzJvx.js} +6833 -966
- package/dist/react-native/chunks/boot-recording.cjs +156 -28
- package/dist/react-native/chunks/boot-recording.js +156 -28
- package/dist/react-native/chunks/get-nitro-module.cjs +12 -0
- package/dist/react-native/chunks/get-nitro-module.js +13 -0
- 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 +39 -3
- 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 +209 -25
- package/src/react-native/network-inspector.ts +2 -2
- package/src/react-native/nitro-fetch/get-nitro-module.ts +13 -0
- package/src/react-native/nitro-fetch/nitro-network-inspector.ts +10 -11
- package/src/shared/http-events.ts +40 -1
- package/src/ui/components/CodeBlock.tsx +45 -1
- package/src/ui/components/FilterBar.tsx +366 -58
- package/src/ui/components/HexView.tsx +54 -0
- package/src/ui/components/MetadataCard.tsx +95 -0
- package/src/ui/components/RequestList.tsx +192 -34
- package/src/ui/components/SidePanel.tsx +42 -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/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/derived.ts +12 -0
- package/src/ui/state/model.ts +6 -1
- package/src/ui/state/store.ts +39 -2
- package/src/ui/tabs/InitiatorTab.tsx +230 -0
- package/src/ui/tabs/ResponseTab.tsx +80 -96
- 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__/symbolication.test.ts +207 -0
- package/src/ui/utils/download.ts +154 -0
- package/src/ui/utils/hex.ts +59 -0
- package/src/ui/utils/initiator.ts +136 -0
- package/src/ui/utils/symbolication.ts +248 -0
- package/src/ui/views/InspectorView.tsx +8 -5
- package/src/utils/__tests__/getContentTypeMimeType.test.ts +65 -0
- package/src/utils/getContentTypeMimeType.ts +28 -0
- package/vite.config.ts +9 -1
- package/vitest.setup.ts +31 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
2
|
+
import type { ResponseRenderer } from './types';
|
|
3
|
+
|
|
4
|
+
export const svgRenderer: ResponseRenderer = {
|
|
5
|
+
id: 'svg',
|
|
6
|
+
matches: (contentType, body) =>
|
|
7
|
+
typeof body === 'string' && contentType.startsWith('image/svg+xml'),
|
|
8
|
+
views: ['preview', 'raw'],
|
|
9
|
+
defaultView: 'preview',
|
|
10
|
+
supportsOverride: false,
|
|
11
|
+
render: ({ view, body }) => {
|
|
12
|
+
if (typeof body !== 'string') return null;
|
|
13
|
+
if (view === 'preview') {
|
|
14
|
+
// <img>-embedded SVG runs no scripts in any major browser — this
|
|
15
|
+
// is the safe way to render SVG from untrusted servers.
|
|
16
|
+
const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(body)}`;
|
|
17
|
+
return (
|
|
18
|
+
<img
|
|
19
|
+
src={dataUrl}
|
|
20
|
+
alt="SVG response"
|
|
21
|
+
className="max-w-full max-h-[400px] object-contain bg-gray-800 rounded-md border border-gray-700 p-2"
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return <CodeBlock>{body}</CodeBlock>;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
2
|
+
import type { ResponseRenderer } from './types';
|
|
3
|
+
|
|
4
|
+
export const textFallbackRenderer: ResponseRenderer = {
|
|
5
|
+
id: 'text-fallback',
|
|
6
|
+
matches: (_contentType, body) => typeof body === 'string',
|
|
7
|
+
views: ['raw'],
|
|
8
|
+
defaultView: 'raw',
|
|
9
|
+
supportsOverride: true,
|
|
10
|
+
render: ({ body }) => {
|
|
11
|
+
if (typeof body !== 'string') return null;
|
|
12
|
+
return <CodeBlock>{body}</CodeBlock>;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { HttpHeaders, ResponseBody } from '../../shared/client';
|
|
3
|
+
|
|
4
|
+
export type ResponseView = 'preview' | 'raw';
|
|
5
|
+
|
|
6
|
+
export type RenderCtx = {
|
|
7
|
+
contentType: string;
|
|
8
|
+
url: string;
|
|
9
|
+
// Response headers, used for fields like Content-Length and
|
|
10
|
+
// Content-Disposition filename. Optional so renderers can be
|
|
11
|
+
// tested with minimal fixtures.
|
|
12
|
+
headers?: HttpHeaders;
|
|
13
|
+
// Response size in bytes as reported by capture (may differ from
|
|
14
|
+
// the decoded base64 length when the server gzips on the wire).
|
|
15
|
+
size?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RenderArgs = {
|
|
19
|
+
view: ResponseView;
|
|
20
|
+
body: ResponseBody;
|
|
21
|
+
ctx: RenderCtx;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// A response-format renderer: a self-contained entry the dispatcher
|
|
25
|
+
// picks based on `matches`. Order in the `renderers` array is matching
|
|
26
|
+
// priority — first match wins, so narrower predicates come before
|
|
27
|
+
// wider fallbacks.
|
|
28
|
+
export type ResponseRenderer = {
|
|
29
|
+
id: string;
|
|
30
|
+
matches: (contentType: string, body: ResponseBody) => boolean;
|
|
31
|
+
// Empty when the renderer has no toggleable views (e.g. a single
|
|
32
|
+
// placeholder for empty bodies). The Preview/Raw toggle is hidden
|
|
33
|
+
// unless `views.length > 1`.
|
|
34
|
+
views: ResponseView[];
|
|
35
|
+
defaultView: ResponseView;
|
|
36
|
+
supportsOverride: boolean;
|
|
37
|
+
render: (args: RenderArgs) => ReactNode;
|
|
38
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResponseRenderer } from './types';
|
|
2
|
+
|
|
3
|
+
// Defensive last-resort match — every preceding renderer should already
|
|
4
|
+
// have claimed any well-formed body. Reaching this renderer means the
|
|
5
|
+
// wire format produced a shape no one handles, which is a bug somewhere
|
|
6
|
+
// upstream. The label exposes the content-type for the bug report.
|
|
7
|
+
export const unknownRenderer: ResponseRenderer = {
|
|
8
|
+
id: 'unknown',
|
|
9
|
+
matches: () => true,
|
|
10
|
+
views: [],
|
|
11
|
+
defaultView: 'raw',
|
|
12
|
+
supportsOverride: false,
|
|
13
|
+
render: ({ ctx }) => (
|
|
14
|
+
<div className="text-sm text-gray-400">
|
|
15
|
+
Could not display response (Content-Type: {ctx.contentType || 'unknown'})
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
2
|
+
import { XmlTree } from '../components/XmlTree';
|
|
3
|
+
import { isXmlContentType } from '../../utils/getContentTypeMimeType';
|
|
4
|
+
import type { ResponseRenderer } from './types';
|
|
5
|
+
|
|
6
|
+
// DOMParser does not throw on malformed XML — instead it produces a
|
|
7
|
+
// Document containing a `<parsererror>` element, but with different
|
|
8
|
+
// placement across engines: Chrome/Safari put it as `documentElement`;
|
|
9
|
+
// Firefox nests it under a dedicated namespace. Cover both.
|
|
10
|
+
const FIREFOX_PARSERERROR_NS =
|
|
11
|
+
'http://www.mozilla.org/newlayout/xml/parsererror.xml';
|
|
12
|
+
|
|
13
|
+
const hasParseError = (doc: Document): boolean =>
|
|
14
|
+
doc.documentElement.nodeName === 'parsererror' ||
|
|
15
|
+
doc.getElementsByTagNameNS(FIREFOX_PARSERERROR_NS, 'parsererror').length > 0;
|
|
16
|
+
|
|
17
|
+
export const xmlRenderer: ResponseRenderer = {
|
|
18
|
+
id: 'xml',
|
|
19
|
+
matches: (contentType, body) =>
|
|
20
|
+
typeof body === 'string' && isXmlContentType(contentType),
|
|
21
|
+
views: ['preview', 'raw'],
|
|
22
|
+
defaultView: 'preview',
|
|
23
|
+
supportsOverride: true,
|
|
24
|
+
render: ({ view, body }) => {
|
|
25
|
+
if (typeof body !== 'string') return null;
|
|
26
|
+
const doc = new DOMParser().parseFromString(body, 'application/xml');
|
|
27
|
+
if (hasParseError(doc)) {
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<CodeBlock>{body}</CodeBlock>
|
|
31
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
32
|
+
⚠️ Failed to parse as XML, showing as raw text
|
|
33
|
+
</div>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (view === 'raw') {
|
|
38
|
+
return <CodeBlock>{body}</CodeBlock>;
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<CodeBlock>
|
|
42
|
+
<XmlTree root={doc.documentElement} />
|
|
43
|
+
</CodeBlock>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
};
|
package/src/ui/state/derived.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
|
|
|
18
18
|
id: httpEntry.id,
|
|
19
19
|
type: 'http',
|
|
20
20
|
source: httpEntry.source,
|
|
21
|
+
initiator: httpEntry.initiator,
|
|
21
22
|
name: httpEntry.request.url,
|
|
22
23
|
status: httpEntry.status,
|
|
23
24
|
timestamp: httpEntry.timestamp,
|
|
@@ -25,6 +26,7 @@ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
|
|
|
25
26
|
size: httpEntry.size ?? null,
|
|
26
27
|
method: httpEntry.request.method,
|
|
27
28
|
httpStatus: httpEntry.response?.status,
|
|
29
|
+
contentType: httpEntry.response?.contentType,
|
|
28
30
|
progress: httpEntry.progress,
|
|
29
31
|
});
|
|
30
32
|
} else if (entry.type === 'websocket') {
|
|
@@ -40,12 +42,15 @@ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
|
|
|
40
42
|
size: null,
|
|
41
43
|
method: 'WS',
|
|
42
44
|
httpStatus: 0,
|
|
45
|
+
contentType: undefined,
|
|
43
46
|
});
|
|
44
47
|
} else if (entry.type === 'sse') {
|
|
45
48
|
const sseEntry = entry as SSENetworkEntry;
|
|
46
49
|
requests.push({
|
|
47
50
|
id: sseEntry.id,
|
|
48
51
|
type: 'sse',
|
|
52
|
+
source: sseEntry.source,
|
|
53
|
+
initiator: sseEntry.initiator,
|
|
49
54
|
name: sseEntry.request.url,
|
|
50
55
|
status: sseEntry.status,
|
|
51
56
|
timestamp: sseEntry.timestamp,
|
|
@@ -53,6 +58,7 @@ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
|
|
|
53
58
|
size: null,
|
|
54
59
|
method: 'SSE',
|
|
55
60
|
httpStatus: 0,
|
|
61
|
+
contentType: sseEntry.response?.contentType,
|
|
56
62
|
});
|
|
57
63
|
}
|
|
58
64
|
}
|
|
@@ -80,6 +86,7 @@ export const getRequestSummary = (
|
|
|
80
86
|
id: httpEntry.id,
|
|
81
87
|
type: 'http',
|
|
82
88
|
source: httpEntry.source,
|
|
89
|
+
initiator: httpEntry.initiator,
|
|
83
90
|
name: httpEntry.request.url,
|
|
84
91
|
status: httpEntry.status,
|
|
85
92
|
timestamp: httpEntry.timestamp,
|
|
@@ -87,6 +94,7 @@ export const getRequestSummary = (
|
|
|
87
94
|
size: httpEntry.size ?? null,
|
|
88
95
|
method: httpEntry.request.method,
|
|
89
96
|
httpStatus: httpEntry.response?.status || 0,
|
|
97
|
+
contentType: httpEntry.response?.contentType,
|
|
90
98
|
progress: httpEntry.progress,
|
|
91
99
|
};
|
|
92
100
|
} else if (entry.type === 'websocket') {
|
|
@@ -102,12 +110,15 @@ export const getRequestSummary = (
|
|
|
102
110
|
size: null,
|
|
103
111
|
method: 'WS',
|
|
104
112
|
httpStatus: 0,
|
|
113
|
+
contentType: undefined,
|
|
105
114
|
};
|
|
106
115
|
} else if (entry.type === 'sse') {
|
|
107
116
|
const sseEntry = entry as SSENetworkEntry;
|
|
108
117
|
return {
|
|
109
118
|
id: sseEntry.id,
|
|
110
119
|
type: 'sse',
|
|
120
|
+
source: sseEntry.source,
|
|
121
|
+
initiator: sseEntry.initiator,
|
|
111
122
|
name: sseEntry.request.url,
|
|
112
123
|
status: sseEntry.status,
|
|
113
124
|
timestamp: sseEntry.timestamp,
|
|
@@ -115,6 +126,7 @@ export const getRequestSummary = (
|
|
|
115
126
|
size: null,
|
|
116
127
|
method: 'SSE',
|
|
117
128
|
httpStatus: 0,
|
|
129
|
+
contentType: sseEntry.response?.contentType,
|
|
118
130
|
};
|
|
119
131
|
}
|
|
120
132
|
|
package/src/ui/state/model.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
HttpHeaders,
|
|
5
5
|
RequestPostData,
|
|
6
6
|
NetworkEventSource,
|
|
7
|
+
ResponseBody,
|
|
7
8
|
} from '../../shared/client';
|
|
8
9
|
|
|
9
10
|
export type RequestId = string;
|
|
@@ -21,7 +22,9 @@ export type HttpRequestData = {
|
|
|
21
22
|
|
|
22
23
|
export type HttpResponseData = {
|
|
23
24
|
type: string;
|
|
24
|
-
|
|
25
|
+
// Mirrors the bridge `ResponseBody` minus null — the store only assigns
|
|
26
|
+
// `data` when the wire body is non-null (null body → undefined response.body).
|
|
27
|
+
data: NonNullable<ResponseBody>;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
export type HttpRequest = {
|
|
@@ -140,6 +143,7 @@ export type ProcessedRequest = {
|
|
|
140
143
|
id: RequestId;
|
|
141
144
|
type: NetworkEntryType;
|
|
142
145
|
source?: NetworkEventSource;
|
|
146
|
+
initiator?: Initiator;
|
|
143
147
|
name: string;
|
|
144
148
|
status: HttpStatus | WebSocketStatus | SSEStatus;
|
|
145
149
|
timestamp: Timestamp;
|
|
@@ -147,6 +151,7 @@ export type ProcessedRequest = {
|
|
|
147
151
|
size: number | null;
|
|
148
152
|
method: HttpMethod | 'WS' | 'SSE';
|
|
149
153
|
httpStatus?: number;
|
|
154
|
+
contentType?: string;
|
|
150
155
|
progress?: {
|
|
151
156
|
loaded: number;
|
|
152
157
|
total: number;
|
package/src/ui/state/store.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { getId } from '../utils/getId';
|
|
|
19
19
|
import { assert } from '../utils/assert';
|
|
20
20
|
import { getContentTypeMime } from '../../utils/getContentTypeMimeType';
|
|
21
21
|
import { applyReactNativeRequestHeadersLogic } from '../../utils/applyReactNativeRequestHeadersLogic';
|
|
22
|
+
import { symbolicateInitiator } from '../utils/symbolication';
|
|
22
23
|
|
|
23
24
|
const MAX_WEBSOCKET_MESSAGES_PER_CONNECTION = 32;
|
|
24
25
|
const MAX_SSE_MESSAGES_PER_CONNECTION = 32;
|
|
@@ -128,7 +129,10 @@ export const createNetworkActivityStore = () =>
|
|
|
128
129
|
data as NetworkActivityEventMap['recording-state'];
|
|
129
130
|
const { isRecording, _client } = get();
|
|
130
131
|
if (_client && isRecording !== eventData.isRecording) {
|
|
131
|
-
_client.send(
|
|
132
|
+
_client.send(
|
|
133
|
+
isRecording ? 'network-enable' : 'network-disable',
|
|
134
|
+
{},
|
|
135
|
+
);
|
|
132
136
|
}
|
|
133
137
|
break;
|
|
134
138
|
}
|
|
@@ -177,6 +181,39 @@ export const createNetworkActivityStore = () =>
|
|
|
177
181
|
newEntries.set(eventData.requestId, entry);
|
|
178
182
|
return { networkEntries: newEntries };
|
|
179
183
|
});
|
|
184
|
+
|
|
185
|
+
if (eventData.initiator.symbolicationStatus === 'pending') {
|
|
186
|
+
void symbolicateInitiator(eventData.initiator).then(
|
|
187
|
+
(symbolicatedInitiator) => {
|
|
188
|
+
if (!symbolicatedInitiator) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
set((state) => {
|
|
193
|
+
const entry = state.networkEntries.get(
|
|
194
|
+
eventData.requestId,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
!entry ||
|
|
199
|
+
(entry.type !== 'http' && entry.type !== 'sse') ||
|
|
200
|
+
entry.initiator?.symbolicationStatus !== 'pending'
|
|
201
|
+
) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const updatedEntry = {
|
|
206
|
+
...entry,
|
|
207
|
+
initiator: symbolicatedInitiator,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const newEntries = new Map(state.networkEntries);
|
|
211
|
+
newEntries.set(eventData.requestId, updatedEntry);
|
|
212
|
+
return { networkEntries: newEntries };
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
180
217
|
break;
|
|
181
218
|
}
|
|
182
219
|
|
|
@@ -594,7 +631,7 @@ export const createNetworkActivityStore = () =>
|
|
|
594
631
|
// Subscribe to all events using the unified handler
|
|
595
632
|
const unsubscribeFunctions = [
|
|
596
633
|
client.onMessage('recording-state', (data) =>
|
|
597
|
-
handleEvent('recording-state', data)
|
|
634
|
+
handleEvent('recording-state', data),
|
|
598
635
|
),
|
|
599
636
|
client.onMessage('client-ui-settings', (data) =>
|
|
600
637
|
handleEvent('client-ui-settings', data),
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { ScrollArea } from '../components/ScrollArea';
|
|
2
|
+
import { Section } from '../components/Section';
|
|
3
|
+
import { KeyValueGrid, type KeyValueItem } from '../components/KeyValueGrid';
|
|
4
|
+
import type { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
|
|
5
|
+
import type { Initiator, InitiatorStackFrame } from '../../shared/client';
|
|
6
|
+
import {
|
|
7
|
+
formatFrameLocation,
|
|
8
|
+
formatSourcePath,
|
|
9
|
+
getBestInitiatorFrame,
|
|
10
|
+
getGeneratedFrameLocation,
|
|
11
|
+
getInitiatorLabel,
|
|
12
|
+
getInitiatorLocationLabel,
|
|
13
|
+
getSourceFrameLocation,
|
|
14
|
+
} from '../utils/initiator';
|
|
15
|
+
|
|
16
|
+
export type InitiatorTabProps = {
|
|
17
|
+
selectedRequest: HttpNetworkEntry | SSENetworkEntry;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const formatInitiatorType = (type: string) => {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'script':
|
|
23
|
+
return 'Script';
|
|
24
|
+
case 'other':
|
|
25
|
+
return 'Other';
|
|
26
|
+
default:
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const formatSymbolicationStatus = (initiator?: Initiator) => {
|
|
32
|
+
switch (initiator?.symbolicationStatus) {
|
|
33
|
+
case 'pending':
|
|
34
|
+
return 'Resolving source...';
|
|
35
|
+
case 'complete':
|
|
36
|
+
return 'Resolved';
|
|
37
|
+
case 'failed':
|
|
38
|
+
return 'Failed';
|
|
39
|
+
case 'unavailable':
|
|
40
|
+
return 'Unavailable';
|
|
41
|
+
default:
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getInitiatorItems = (initiator?: Initiator): KeyValueItem[] => {
|
|
47
|
+
if (!initiator) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sourceFrame = getBestInitiatorFrame(initiator);
|
|
52
|
+
const sourceLocation = getInitiatorLocationLabel(initiator);
|
|
53
|
+
const generatedLocation = formatFrameLocation(
|
|
54
|
+
getGeneratedFrameLocation(initiator),
|
|
55
|
+
);
|
|
56
|
+
const status = formatSymbolicationStatus(initiator);
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
key: 'Type',
|
|
61
|
+
value: formatInitiatorType(initiator.type),
|
|
62
|
+
},
|
|
63
|
+
...(status
|
|
64
|
+
? [
|
|
65
|
+
{
|
|
66
|
+
key: 'Source map',
|
|
67
|
+
value: status,
|
|
68
|
+
valueClassName:
|
|
69
|
+
initiator.symbolicationStatus === 'failed'
|
|
70
|
+
? 'text-red-300'
|
|
71
|
+
: 'text-gray-300',
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
: []),
|
|
75
|
+
...(sourceFrame?.functionName
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
key: 'Function',
|
|
79
|
+
value: sourceFrame.functionName,
|
|
80
|
+
valueClassName: 'font-mono text-blue-300',
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
: []),
|
|
84
|
+
...(sourceFrame?.url
|
|
85
|
+
? [
|
|
86
|
+
{
|
|
87
|
+
key: 'Source',
|
|
88
|
+
value: formatSourcePath(sourceFrame.url),
|
|
89
|
+
valueClassName: 'font-mono text-blue-300',
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
: []),
|
|
93
|
+
...(sourceFrame?.lineNumber !== undefined
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
key: 'Line',
|
|
97
|
+
value: sourceFrame.lineNumber,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
: []),
|
|
101
|
+
...(sourceFrame?.columnNumber !== undefined
|
|
102
|
+
? [
|
|
103
|
+
{
|
|
104
|
+
key: 'Column',
|
|
105
|
+
value: sourceFrame.columnNumber,
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
: []),
|
|
109
|
+
...(sourceLocation
|
|
110
|
+
? [
|
|
111
|
+
{
|
|
112
|
+
key: 'Location',
|
|
113
|
+
value: sourceLocation,
|
|
114
|
+
valueClassName: 'font-mono text-blue-300',
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
...(generatedLocation && generatedLocation !== sourceLocation
|
|
119
|
+
? [
|
|
120
|
+
{
|
|
121
|
+
key: 'Generated',
|
|
122
|
+
value: generatedLocation,
|
|
123
|
+
valueClassName: 'font-mono text-gray-500',
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
: []),
|
|
127
|
+
];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const getStackFrameLocation = (frame: InitiatorStackFrame) => {
|
|
131
|
+
const sourceLocation = formatFrameLocation(getSourceFrameLocation(frame));
|
|
132
|
+
const generatedLocation = formatFrameLocation(
|
|
133
|
+
getGeneratedFrameLocation(frame),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
sourceLocation &&
|
|
138
|
+
generatedLocation &&
|
|
139
|
+
sourceLocation !== generatedLocation
|
|
140
|
+
) {
|
|
141
|
+
return (
|
|
142
|
+
<span>
|
|
143
|
+
{sourceLocation}
|
|
144
|
+
<span className="ml-2 text-gray-500">
|
|
145
|
+
generated {generatedLocation}
|
|
146
|
+
</span>
|
|
147
|
+
</span>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return sourceLocation ?? generatedLocation ?? 'Unknown location';
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const getStackItems = (initiator?: Initiator): KeyValueItem[] => {
|
|
155
|
+
return (
|
|
156
|
+
initiator?.stack?.map((frame, index) => {
|
|
157
|
+
return {
|
|
158
|
+
key: frame.functionName || `Frame ${index + 1}`,
|
|
159
|
+
value: getStackFrameLocation(frame),
|
|
160
|
+
keyClassName: 'font-mono',
|
|
161
|
+
valueClassName: 'font-mono text-blue-300',
|
|
162
|
+
};
|
|
163
|
+
}) ?? []
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const InitiatorTab = ({ selectedRequest }: InitiatorTabProps) => {
|
|
168
|
+
const initiator = selectedRequest.initiator;
|
|
169
|
+
const initiatorItems = getInitiatorItems(initiator);
|
|
170
|
+
const stackItems = getStackItems(initiator);
|
|
171
|
+
const initiatorLabel = getInitiatorLabel(initiator);
|
|
172
|
+
const initiatorLocation = getInitiatorLocationLabel(initiator);
|
|
173
|
+
const hasSourceMappedFrame = Boolean(
|
|
174
|
+
getSourceFrameLocation(initiator) ||
|
|
175
|
+
initiator?.stack?.some((frame) => getSourceFrameLocation(frame)),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<ScrollArea className="h-full w-full">
|
|
180
|
+
<div className="p-4 space-y-4">
|
|
181
|
+
<div className="rounded-md border border-gray-700 bg-gray-800/60 p-3">
|
|
182
|
+
<div className="text-xs uppercase text-gray-500">Triggered by</div>
|
|
183
|
+
<div className="mt-1 font-mono text-sm text-blue-300">
|
|
184
|
+
{initiatorLabel ?? 'Unknown initiator'}
|
|
185
|
+
</div>
|
|
186
|
+
{initiatorLocation && initiatorLocation !== initiatorLabel && (
|
|
187
|
+
<div className="mt-1 font-mono text-xs text-gray-400 wrap-anywhere">
|
|
188
|
+
{initiatorLocation}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<Section title="Initiator">
|
|
194
|
+
<KeyValueGrid
|
|
195
|
+
items={initiatorItems}
|
|
196
|
+
emptyMessage="No initiator metadata available"
|
|
197
|
+
/>
|
|
198
|
+
</Section>
|
|
199
|
+
|
|
200
|
+
{!hasSourceMappedFrame &&
|
|
201
|
+
initiator?.symbolicationStatus !== 'pending' && (
|
|
202
|
+
<div className="rounded-md border border-gray-700 bg-gray-800/60 p-3 text-sm text-gray-400">
|
|
203
|
+
This request only includes generated bundle location data. Metro
|
|
204
|
+
source maps were not available for this entry.
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{initiator?.symbolicationError && (
|
|
209
|
+
<div className="rounded-md border border-red-900/70 bg-red-950/30 p-3 text-sm text-red-200">
|
|
210
|
+
{initiator.symbolicationError}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{initiator?.codeFrame && (
|
|
215
|
+
<Section title="Code Frame">
|
|
216
|
+
<pre className="overflow-auto rounded-md bg-gray-950 p-3 text-xs text-gray-300">
|
|
217
|
+
<code>{initiator.codeFrame.content}</code>
|
|
218
|
+
</pre>
|
|
219
|
+
</Section>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{stackItems.length > 0 && (
|
|
223
|
+
<Section title="Stack Preview">
|
|
224
|
+
<KeyValueGrid items={stackItems} />
|
|
225
|
+
</Section>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</ScrollArea>
|
|
229
|
+
);
|
|
230
|
+
};
|