@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/devtools/App.html +2 -2
  3. package/dist/devtools/assets/{App-m6xge0az.css → App-CUXU0mup.css} +152 -2
  4. package/dist/devtools/assets/{App-B3xlUjs6.js → App-DsimzJvx.js} +6833 -966
  5. package/dist/react-native/chunks/boot-recording.cjs +156 -28
  6. package/dist/react-native/chunks/boot-recording.js +156 -28
  7. package/dist/react-native/chunks/get-nitro-module.cjs +12 -0
  8. package/dist/react-native/chunks/get-nitro-module.js +13 -0
  9. package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +20 -1
  10. package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +20 -1
  11. package/dist/react-native/index.d.ts +39 -3
  12. package/dist/rozenite.json +1 -1
  13. package/dist/sdk/index.d.ts +37 -1
  14. package/package.json +12 -7
  15. package/src/react-native/agent/use-network-activity-agent-tools.ts +22 -4
  16. package/src/react-native/http/__tests__/http-utils.test.ts +228 -0
  17. package/src/react-native/http/http-utils.ts +209 -25
  18. package/src/react-native/network-inspector.ts +2 -2
  19. package/src/react-native/nitro-fetch/get-nitro-module.ts +13 -0
  20. package/src/react-native/nitro-fetch/nitro-network-inspector.ts +10 -11
  21. package/src/shared/http-events.ts +40 -1
  22. package/src/ui/components/CodeBlock.tsx +45 -1
  23. package/src/ui/components/FilterBar.tsx +366 -58
  24. package/src/ui/components/HexView.tsx +54 -0
  25. package/src/ui/components/MetadataCard.tsx +95 -0
  26. package/src/ui/components/RequestList.tsx +192 -34
  27. package/src/ui/components/SidePanel.tsx +42 -1
  28. package/src/ui/components/ViewToggle.tsx +44 -0
  29. package/src/ui/components/XmlTree.tsx +160 -0
  30. package/src/ui/components/__tests__/CodeBlock.test.tsx +89 -0
  31. package/src/ui/components/__tests__/HexView.test.tsx +41 -0
  32. package/src/ui/components/__tests__/MetadataCard.test.tsx +107 -0
  33. package/src/ui/components/__tests__/ViewToggle.test.tsx +80 -0
  34. package/src/ui/components/__tests__/XmlTree.test.tsx +149 -0
  35. package/src/ui/response-renderers/__tests__/binary-too-large.test.tsx +56 -0
  36. package/src/ui/response-renderers/__tests__/binary.test.tsx +96 -0
  37. package/src/ui/response-renderers/__tests__/dispatch.test.ts +124 -0
  38. package/src/ui/response-renderers/__tests__/html.test.tsx +101 -0
  39. package/src/ui/response-renderers/__tests__/image.test.tsx +73 -0
  40. package/src/ui/response-renderers/__tests__/json.test.tsx +95 -0
  41. package/src/ui/response-renderers/__tests__/svg.test.tsx +46 -0
  42. package/src/ui/response-renderers/__tests__/xml.test.tsx +100 -0
  43. package/src/ui/response-renderers/binary-too-large.tsx +36 -0
  44. package/src/ui/response-renderers/binary.tsx +31 -0
  45. package/src/ui/response-renderers/empty.tsx +14 -0
  46. package/src/ui/response-renderers/html.tsx +36 -0
  47. package/src/ui/response-renderers/image.tsx +37 -0
  48. package/src/ui/response-renderers/index.ts +55 -0
  49. package/src/ui/response-renderers/json.tsx +40 -0
  50. package/src/ui/response-renderers/svg.tsx +27 -0
  51. package/src/ui/response-renderers/text-fallback.tsx +14 -0
  52. package/src/ui/response-renderers/types.ts +38 -0
  53. package/src/ui/response-renderers/unknown.tsx +18 -0
  54. package/src/ui/response-renderers/xml.tsx +46 -0
  55. package/src/ui/state/derived.ts +12 -0
  56. package/src/ui/state/model.ts +6 -1
  57. package/src/ui/state/store.ts +39 -2
  58. package/src/ui/tabs/InitiatorTab.tsx +230 -0
  59. package/src/ui/tabs/ResponseTab.tsx +80 -96
  60. package/src/ui/tabs/__tests__/ResponseTab.test.tsx +102 -0
  61. package/src/ui/utils/__tests__/download.test.ts +115 -0
  62. package/src/ui/utils/__tests__/hex.test.ts +84 -0
  63. package/src/ui/utils/__tests__/symbolication.test.ts +207 -0
  64. package/src/ui/utils/download.ts +154 -0
  65. package/src/ui/utils/hex.ts +59 -0
  66. package/src/ui/utils/initiator.ts +136 -0
  67. package/src/ui/utils/symbolication.ts +248 -0
  68. package/src/ui/views/InspectorView.tsx +8 -5
  69. package/src/utils/__tests__/getContentTypeMimeType.test.ts +65 -0
  70. package/src/utils/getContentTypeMimeType.ts +28 -0
  71. package/vite.config.ts +9 -1
  72. 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
+ };
@@ -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
 
@@ -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
- data: string;
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;
@@ -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(isRecording ? 'network-enable' : 'network-disable', {});
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
+ };