Skip to content

Commit dd2331a

Browse files
cmcWebCode40claude
andcommitted
feat: add export utilities for HAR, Postman, and JSON formats
Support exporting network logs to HAR format (for browser DevTools), Postman collections (for API testing), and raw JSON. Includes proper header/query string parsing and content type detection. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 005656d commit dd2331a

1 file changed

Lines changed: 302 additions & 0 deletions

File tree

src/utils/export.ts

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import type { NetworkLog } from '../types';
2+
3+
export interface HARLog {
4+
version: string;
5+
creator: {
6+
name: string;
7+
version: string;
8+
};
9+
entries: HAREntry[];
10+
}
11+
12+
export interface HAREntry {
13+
startedDateTime: string;
14+
time: number;
15+
request: {
16+
method: string;
17+
url: string;
18+
httpVersion: string;
19+
headers: { name: string; value: string }[];
20+
queryString: { name: string; value: string }[];
21+
postData?: {
22+
mimeType: string;
23+
text: string;
24+
};
25+
headersSize: number;
26+
bodySize: number;
27+
};
28+
response: {
29+
status: number;
30+
statusText: string;
31+
httpVersion: string;
32+
headers: { name: string; value: string }[];
33+
content: {
34+
size: number;
35+
mimeType: string;
36+
text: string;
37+
};
38+
headersSize: number;
39+
bodySize: number;
40+
};
41+
cache: Record<string, never>;
42+
timings: {
43+
send: number;
44+
wait: number;
45+
receive: number;
46+
};
47+
}
48+
49+
const parseQueryString = (url: string): { name: string; value: string }[] => {
50+
try {
51+
const urlObj = new URL(url);
52+
const params: { name: string; value: string }[] = [];
53+
urlObj.searchParams.forEach((value, name) => {
54+
params.push({ name, value });
55+
});
56+
return params;
57+
} catch {
58+
return [];
59+
}
60+
};
61+
62+
const headersToArray = (
63+
headers: Record<string, string>
64+
): { name: string; value: string }[] => {
65+
return Object.entries(headers).map(([name, value]) => ({ name, value }));
66+
};
67+
68+
const getContentType = (headers: Record<string, string>): string => {
69+
const contentType = Object.entries(headers).find(
70+
([key]) => key.toLowerCase() === 'content-type'
71+
);
72+
return contentType ? contentType[1] : 'application/octet-stream';
73+
};
74+
75+
export const networkLogToHAREntry = (log: NetworkLog): HAREntry => {
76+
const duration = log.response?.duration || log.duration || 0;
77+
const requestHeaders = headersToArray(log.headers || {});
78+
const responseHeaders = headersToArray(log.response?.headers || {});
79+
80+
return {
81+
startedDateTime: log.timestamp,
82+
time: duration,
83+
request: {
84+
method: log.method,
85+
url: log.url,
86+
httpVersion: 'HTTP/1.1',
87+
headers: requestHeaders,
88+
queryString: parseQueryString(log.url),
89+
...(log.body && {
90+
postData: {
91+
mimeType: getContentType(log.headers || {}),
92+
text: log.body,
93+
},
94+
}),
95+
headersSize: JSON.stringify(requestHeaders).length,
96+
bodySize: log.body ? log.body.length : 0,
97+
},
98+
response: {
99+
status: log.response?.status || 0,
100+
statusText: log.response?.statusText || '',
101+
httpVersion: 'HTTP/1.1',
102+
headers: responseHeaders,
103+
content: {
104+
size: log.response?.body?.length || 0,
105+
mimeType: getContentType(log.response?.headers || {}),
106+
text: log.response?.body || '',
107+
},
108+
headersSize: JSON.stringify(responseHeaders).length,
109+
bodySize: log.response?.body?.length || 0,
110+
},
111+
cache: {},
112+
timings: {
113+
send: 0,
114+
wait: duration,
115+
receive: 0,
116+
},
117+
};
118+
};
119+
120+
export const exportToHAR = (logs: NetworkLog[]): string => {
121+
const har: { log: HARLog } = {
122+
log: {
123+
version: '1.2',
124+
creator: {
125+
name: 'react-native-api-debugger',
126+
version: '1.0.0',
127+
},
128+
entries: logs.map(networkLogToHAREntry),
129+
},
130+
};
131+
132+
return JSON.stringify(har, null, 2);
133+
};
134+
135+
export interface PostmanCollection {
136+
info: {
137+
name: string;
138+
schema: string;
139+
_postman_id?: string;
140+
};
141+
item: PostmanItem[];
142+
}
143+
144+
export interface PostmanItem {
145+
name: string;
146+
request: {
147+
method: string;
148+
header: { key: string; value: string }[];
149+
url: {
150+
raw: string;
151+
protocol?: string;
152+
host?: string[];
153+
path?: string[];
154+
query?: { key: string; value: string }[];
155+
};
156+
body?: {
157+
mode: string;
158+
raw: string;
159+
options?: {
160+
raw: {
161+
language: string;
162+
};
163+
};
164+
};
165+
};
166+
response: never[];
167+
}
168+
169+
const parseUrlForPostman = (
170+
url: string
171+
): {
172+
raw: string;
173+
protocol?: string;
174+
host?: string[];
175+
path?: string[];
176+
query?: { key: string; value: string }[];
177+
} => {
178+
try {
179+
const urlObj = new URL(url);
180+
const query: { key: string; value: string }[] = [];
181+
urlObj.searchParams.forEach((value, key) => {
182+
query.push({ key, value });
183+
});
184+
185+
return {
186+
raw: url,
187+
protocol: urlObj.protocol.replace(':', ''),
188+
host: urlObj.hostname.split('.'),
189+
path: urlObj.pathname.split('/').filter(Boolean),
190+
...(query.length > 0 && { query }),
191+
};
192+
} catch {
193+
return { raw: url };
194+
}
195+
};
196+
197+
export const networkLogToPostmanItem = (log: NetworkLog): PostmanItem => {
198+
const headers = Object.entries(log.headers || {}).map(([key, value]) => ({
199+
key,
200+
value,
201+
}));
202+
203+
const item: PostmanItem = {
204+
name: `${log.method} ${new URL(log.url).pathname}`,
205+
request: {
206+
method: log.method,
207+
header: headers,
208+
url: parseUrlForPostman(log.url),
209+
},
210+
response: [],
211+
};
212+
213+
if (log.body) {
214+
const contentType = getContentType(log.headers || {});
215+
const isJson = contentType.includes('application/json');
216+
217+
item.request.body = {
218+
mode: 'raw',
219+
raw: log.body,
220+
options: {
221+
raw: {
222+
language: isJson ? 'json' : 'text',
223+
},
224+
},
225+
};
226+
}
227+
228+
return item;
229+
};
230+
231+
export const exportToPostman = (
232+
logs: NetworkLog[],
233+
collectionName: string = 'API Debugger Export'
234+
): string => {
235+
const collection: PostmanCollection = {
236+
info: {
237+
name: collectionName,
238+
schema:
239+
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
240+
},
241+
item: logs.map(networkLogToPostmanItem),
242+
};
243+
244+
return JSON.stringify(collection, null, 2);
245+
};
246+
247+
export const exportSingleRequestAsJSON = (log: NetworkLog): string => {
248+
return JSON.stringify(
249+
{
250+
method: log.method,
251+
url: log.url,
252+
headers: log.headers,
253+
body: log.body,
254+
timestamp: log.timestamp,
255+
response: log.response
256+
? {
257+
status: log.response.status,
258+
statusText: log.response.statusText,
259+
headers: log.response.headers,
260+
body: log.response.body,
261+
duration: log.response.duration,
262+
}
263+
: null,
264+
error: log.error,
265+
},
266+
null,
267+
2
268+
);
269+
};
270+
271+
export type ExportFormat = 'har' | 'postman' | 'json';
272+
273+
export const exportLogs = (
274+
logs: NetworkLog[],
275+
format: ExportFormat,
276+
options?: { collectionName?: string }
277+
): string => {
278+
switch (format) {
279+
case 'har':
280+
return exportToHAR(logs);
281+
case 'postman':
282+
return exportToPostman(logs, options?.collectionName);
283+
case 'json':
284+
return JSON.stringify(logs, null, 2);
285+
default:
286+
return JSON.stringify(logs, null, 2);
287+
}
288+
};
289+
290+
export const getExportFileName = (format: ExportFormat): string => {
291+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
292+
switch (format) {
293+
case 'har':
294+
return `network-logs-${timestamp}.har`;
295+
case 'postman':
296+
return `postman-collection-${timestamp}.json`;
297+
case 'json':
298+
return `network-logs-${timestamp}.json`;
299+
default:
300+
return `network-logs-${timestamp}.json`;
301+
}
302+
};

0 commit comments

Comments
 (0)