@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,207 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ symbolicateInitiator,
4
+ symbolicateStackTraceWithMetro,
5
+ } from '../symbolication';
6
+ import type { Initiator } from '../../../shared/client';
7
+
8
+ describe('symbolication', () => {
9
+ it('selects the source frame matching the Metro code frame', async () => {
10
+ const initiator: Initiator = {
11
+ type: 'script',
12
+ generatedUrl: 'http://localhost:8081/index.bundle',
13
+ generatedLineNumber: 1,
14
+ generatedColumnNumber: 100,
15
+ symbolicationStatus: 'pending',
16
+ stack: [
17
+ {
18
+ functionName: 'fetch',
19
+ generatedUrl: 'http://localhost:8081/index.bundle',
20
+ generatedLineNumber: 1,
21
+ generatedColumnNumber: 100,
22
+ },
23
+ {
24
+ functionName: 'loadUsers',
25
+ generatedUrl: 'http://localhost:8081/index.bundle',
26
+ generatedLineNumber: 1,
27
+ generatedColumnNumber: 200,
28
+ },
29
+ ],
30
+ };
31
+
32
+ const symbolicatedInitiator = await symbolicateInitiator(
33
+ initiator,
34
+ vi.fn().mockResolvedValue({
35
+ stack: [
36
+ {
37
+ methodName: 'fetch',
38
+ file: 'node_modules/react-native/Libraries/Network/fetch.js',
39
+ lineNumber: 10,
40
+ column: 2,
41
+ },
42
+ {
43
+ methodName: 'loadUsers',
44
+ file: 'apps/playground/src/app/api.ts',
45
+ lineNumber: 30,
46
+ column: 6,
47
+ },
48
+ ],
49
+ codeFrame: {
50
+ content: '\u001b[90m 30 |\u001b[39m loadUsers();\u001b[0m',
51
+ fileName: 'apps/playground/src/app/api.ts',
52
+ location: {
53
+ row: 30,
54
+ column: 6,
55
+ },
56
+ },
57
+ }),
58
+ );
59
+
60
+ expect(symbolicatedInitiator).toMatchObject({
61
+ type: 'script',
62
+ functionName: 'loadUsers',
63
+ url: 'apps/playground/src/app/api.ts',
64
+ lineNumber: 30,
65
+ columnNumber: 6,
66
+ symbolicationStatus: 'complete',
67
+ codeFrame: {
68
+ content: ' 30 | loadUsers();',
69
+ fileName: 'apps/playground/src/app/api.ts',
70
+ },
71
+ });
72
+ });
73
+
74
+ it('falls back to the first non-collapsed source frame', async () => {
75
+ const initiator: Initiator = {
76
+ type: 'script',
77
+ generatedUrl: 'http://localhost:8081/index.bundle',
78
+ generatedLineNumber: 1,
79
+ generatedColumnNumber: 100,
80
+ symbolicationStatus: 'pending',
81
+ stack: [
82
+ {
83
+ functionName: 'send',
84
+ generatedUrl: 'http://localhost:8081/index.bundle',
85
+ generatedLineNumber: 1,
86
+ generatedColumnNumber: 100,
87
+ },
88
+ {
89
+ functionName: 'loadUsers',
90
+ generatedUrl: 'http://localhost:8081/index.bundle',
91
+ generatedLineNumber: 1,
92
+ generatedColumnNumber: 200,
93
+ },
94
+ ],
95
+ };
96
+
97
+ const symbolicatedInitiator = await symbolicateInitiator(
98
+ initiator,
99
+ vi.fn().mockResolvedValue({
100
+ stack: [
101
+ {
102
+ methodName: 'send',
103
+ file: 'packages/network-activity-plugin/src/http.ts',
104
+ lineNumber: 10,
105
+ column: 2,
106
+ collapse: true,
107
+ },
108
+ {
109
+ methodName: 'loadUsers',
110
+ file: 'apps/playground/src/app/api.ts',
111
+ lineNumber: 30,
112
+ column: 6,
113
+ },
114
+ ],
115
+ }),
116
+ );
117
+
118
+ expect(symbolicatedInitiator).toMatchObject({
119
+ type: 'script',
120
+ functionName: 'loadUsers',
121
+ url: 'apps/playground/src/app/api.ts',
122
+ lineNumber: 30,
123
+ columnNumber: 6,
124
+ symbolicationStatus: 'complete',
125
+ codeFrame: null,
126
+ });
127
+ });
128
+
129
+ it('reports symbolication failures on the initiator', async () => {
130
+ const initiator: Initiator = {
131
+ type: 'script',
132
+ symbolicationStatus: 'pending',
133
+ stack: [
134
+ {
135
+ generatedUrl: 'http://localhost:8081/index.bundle',
136
+ generatedLineNumber: 1,
137
+ generatedColumnNumber: 100,
138
+ },
139
+ ],
140
+ };
141
+
142
+ const symbolicatedInitiator = await symbolicateInitiator(
143
+ initiator,
144
+ vi.fn().mockRejectedValue(new Error('Metro is unavailable')),
145
+ );
146
+
147
+ expect(symbolicatedInitiator).toMatchObject({
148
+ symbolicationStatus: 'failed',
149
+ symbolicationError: 'Metro is unavailable',
150
+ });
151
+ });
152
+
153
+ it('posts stack frames to the Metro symbolication endpoint from the panel origin', async () => {
154
+ const originalFetch = globalThis.fetch;
155
+ const originalWindow = globalThis.window;
156
+
157
+ Object.defineProperty(globalThis, 'window', {
158
+ value: {
159
+ location: {
160
+ origin: 'http://localhost:8081',
161
+ },
162
+ },
163
+ configurable: true,
164
+ });
165
+
166
+ globalThis.fetch = vi.fn().mockResolvedValue({
167
+ ok: true,
168
+ json: vi.fn().mockResolvedValue({
169
+ stack: [],
170
+ }),
171
+ } as Partial<Response> as Response);
172
+
173
+ try {
174
+ await symbolicateStackTraceWithMetro([
175
+ {
176
+ methodName: 'loadUsers',
177
+ file: 'http://localhost:8081/index.bundle',
178
+ lineNumber: 1,
179
+ column: 100,
180
+ },
181
+ ]);
182
+
183
+ expect(globalThis.fetch).toHaveBeenCalledWith(
184
+ 'http://localhost:8081/symbolicate',
185
+ expect.objectContaining({
186
+ method: 'POST',
187
+ body: JSON.stringify({
188
+ stack: [
189
+ {
190
+ methodName: 'loadUsers',
191
+ file: 'http://localhost:8081/index.bundle',
192
+ lineNumber: 1,
193
+ column: 100,
194
+ },
195
+ ],
196
+ }),
197
+ }),
198
+ );
199
+ } finally {
200
+ globalThis.fetch = originalFetch;
201
+ Object.defineProperty(globalThis, 'window', {
202
+ value: originalWindow,
203
+ configurable: true,
204
+ });
205
+ }
206
+ });
207
+ });
@@ -0,0 +1,154 @@
1
+ import type { HttpHeaders } from '../../shared/client';
2
+
3
+ // Decode a base64 string to a Uint8Array via `atob`. Mirrors the
4
+ // chunking concern on the encode side: atob produces a binary string,
5
+ // each char is one byte, we re-pack into a typed array byte by byte.
6
+ export const base64ToBytes = (base64: string): Uint8Array => {
7
+ const binary = atob(base64);
8
+ const bytes = new Uint8Array(binary.length);
9
+ for (let i = 0; i < binary.length; i++) {
10
+ bytes[i] = binary.charCodeAt(i);
11
+ }
12
+ return bytes;
13
+ };
14
+
15
+ export const base64ToBlob = (base64: string, contentType: string): Blob => {
16
+ // Allocate the ArrayBuffer up front so the Uint8Array is known to
17
+ // be ArrayBuffer-backed (not SharedArrayBuffer); satisfies the
18
+ // BlobPart constraint without a cast.
19
+ const binary = atob(base64);
20
+ const buffer = new ArrayBuffer(binary.length);
21
+ const view = new Uint8Array(buffer);
22
+ for (let i = 0; i < binary.length; i++) {
23
+ view[i] = binary.charCodeAt(i);
24
+ }
25
+ return new Blob([buffer], {
26
+ type: contentType || 'application/octet-stream',
27
+ });
28
+ };
29
+
30
+ // Fallback extension when neither Content-Disposition nor URL gives us
31
+ // a usable filename. Map a few common content-types so saved files open
32
+ // in the right tool; everything else lands as `.bin`.
33
+ const CONTENT_TYPE_EXTENSIONS: Record<string, string> = {
34
+ 'application/pdf': 'pdf',
35
+ 'application/zip': 'zip',
36
+ 'application/gzip': 'gz',
37
+ 'application/json': 'json',
38
+ 'application/xml': 'xml',
39
+ 'application/javascript': 'js',
40
+ 'application/octet-stream': 'bin',
41
+ 'image/png': 'png',
42
+ 'image/jpeg': 'jpg',
43
+ 'image/gif': 'gif',
44
+ 'image/webp': 'webp',
45
+ 'image/svg+xml': 'svg',
46
+ 'image/bmp': 'bmp',
47
+ 'image/x-icon': 'ico',
48
+ 'audio/mpeg': 'mp3',
49
+ 'audio/ogg': 'ogg',
50
+ 'audio/wav': 'wav',
51
+ 'video/mp4': 'mp4',
52
+ 'video/webm': 'webm',
53
+ 'font/woff': 'woff',
54
+ 'font/woff2': 'woff2',
55
+ 'font/ttf': 'ttf',
56
+ 'font/otf': 'otf',
57
+ 'text/html': 'html',
58
+ 'text/plain': 'txt',
59
+ 'text/css': 'css',
60
+ 'text/csv': 'csv',
61
+ };
62
+
63
+ const extensionForContentType = (contentType: string): string => {
64
+ // Strip parameters: "text/html; charset=utf-8" → "text/html".
65
+ const bare = contentType.split(';', 1)[0]?.trim().toLowerCase() ?? '';
66
+ return CONTENT_TYPE_EXTENSIONS[bare] ?? 'bin';
67
+ };
68
+
69
+ export const readHeader = (
70
+ headers: HttpHeaders | undefined,
71
+ name: string,
72
+ ): string | undefined => {
73
+ if (!headers) return undefined;
74
+ const lowerTarget = name.toLowerCase();
75
+ for (const [key, value] of Object.entries(headers)) {
76
+ if (key.toLowerCase() === lowerTarget) {
77
+ return Array.isArray(value) ? value[0] : value;
78
+ }
79
+ }
80
+ return undefined;
81
+ };
82
+
83
+ // RFC 6266: `Content-Disposition: attachment; filename="report.pdf"`
84
+ // or `filename*=UTF-8''report%20with%20space.pdf`. We only extract the
85
+ // raw value here; downstream callers can sanitize further if they care
86
+ // about path traversal etc. (irrelevant for the playground / debug use
87
+ // case but worth knowing).
88
+ const parseContentDispositionFilename = (
89
+ header: string | undefined,
90
+ ): string | undefined => {
91
+ if (!header) return undefined;
92
+ // Prefer RFC 5987 `filename*` over the legacy `filename` when both
93
+ // are present — it has a stricter encoding contract.
94
+ const extended = /filename\*\s*=\s*[^']*''([^;]+)/i.exec(header);
95
+ if (extended?.[1]) {
96
+ try {
97
+ return decodeURIComponent(extended[1].trim()) || undefined;
98
+ } catch {
99
+ // Fall through to the unencoded form.
100
+ }
101
+ }
102
+ const basic = /filename\s*=\s*("([^"]*)"|([^;]+))/i.exec(header);
103
+ const value = basic?.[2] ?? basic?.[3];
104
+ return value?.trim() || undefined;
105
+ };
106
+
107
+ const filenameFromUrl = (url: string): string | undefined => {
108
+ try {
109
+ const parsed = new URL(url);
110
+ const segments = parsed.pathname.split('/').filter(Boolean);
111
+ const last = segments[segments.length - 1];
112
+ return last && last.length > 0 ? last : undefined;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ };
117
+
118
+ // Three-tier filename derivation:
119
+ // 1. Content-Disposition filename (RFC 5987 → RFC 6266)
120
+ // 2. Last path segment of the response URL
121
+ // 3. `response.<ext>` where the extension comes from a small
122
+ // Content-Type → extension map (everything unknown becomes `.bin`)
123
+ export const deriveFilename = ({
124
+ headers,
125
+ url,
126
+ contentType,
127
+ }: {
128
+ headers?: HttpHeaders;
129
+ url: string;
130
+ contentType: string;
131
+ }): string => {
132
+ const fromDisposition = parseContentDispositionFilename(
133
+ readHeader(headers, 'Content-Disposition'),
134
+ );
135
+ if (fromDisposition) return fromDisposition;
136
+
137
+ const fromUrl = filenameFromUrl(url);
138
+ if (fromUrl) return fromUrl;
139
+
140
+ return `response.${extensionForContentType(contentType)}`;
141
+ };
142
+
143
+ export const downloadBlob = (blob: Blob, filename: string): void => {
144
+ const objectUrl = URL.createObjectURL(blob);
145
+ const anchor = document.createElement('a');
146
+ anchor.href = objectUrl;
147
+ anchor.download = filename;
148
+ document.body.appendChild(anchor);
149
+ anchor.click();
150
+ document.body.removeChild(anchor);
151
+ // Defer revoke a tick so Safari's download pipeline doesn't drop
152
+ // the request when the URL disappears mid-click.
153
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
154
+ };
@@ -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
+ };