Skip to content

Commit 005656d

Browse files
cmcWebCode40claude
andcommitted
feat: add utility modules for filtering, cURL generation, and formatting
Add filter utilities supporting status code filtering (2xx-5xx), method filtering, search across URL/body/headers, and log statistics. Include cURL command generation and data formatting helpers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 26b07b6 commit 005656d

4 files changed

Lines changed: 313 additions & 0 deletions

File tree

src/utils/curl.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { NetworkLog } from '../types';
2+
3+
export const generateCurl = (log: NetworkLog): string => {
4+
let curl = `curl -X ${log.method}`;
5+
6+
if (log.headers && typeof log.headers === 'object') {
7+
Object.entries(log.headers).forEach(([key, value]: [string, string]) => {
8+
const escapedValue = value.replace(/"/g, '\\"');
9+
curl += ` \\\n -H "${key}: ${escapedValue}"`;
10+
});
11+
}
12+
13+
if (log.body) {
14+
const escapedBody = log.body.replace(/'/g, "'\\''");
15+
curl += ` \\\n -d '${escapedBody}'`;
16+
}
17+
18+
curl += ` \\\n "${log.url}"`;
19+
20+
return curl;
21+
};
22+
23+
export const generateCurlOneLine = (log: NetworkLog): string => {
24+
let curl = `curl -X ${log.method}`;
25+
26+
if (log.headers && typeof log.headers === 'object') {
27+
Object.entries(log.headers).forEach(([key, value]: [string, string]) => {
28+
const escapedValue = value.replace(/"/g, '\\"');
29+
curl += ` -H "${key}: ${escapedValue}"`;
30+
});
31+
}
32+
33+
if (log.body) {
34+
const escapedBody = log.body.replace(/'/g, "'\\''");
35+
curl += ` -d '${escapedBody}'`;
36+
}
37+
38+
curl += ` "${log.url}"`;
39+
40+
return curl;
41+
};
42+
43+
export const parseCurlCommand = (
44+
curlCommand: string
45+
): {
46+
method: string;
47+
url: string;
48+
headers: Record<string, string>;
49+
body: string | null;
50+
} => {
51+
const result = {
52+
method: 'GET',
53+
url: '',
54+
headers: {} as Record<string, string>,
55+
body: null as string | null,
56+
};
57+
58+
const methodMatch = curlCommand.match(/-X\s+(\w+)/);
59+
if (methodMatch && methodMatch[1]) {
60+
result.method = methodMatch[1];
61+
}
62+
63+
const headerMatches = curlCommand.matchAll(/-H\s+"([^:]+):\s*([^"]+)"/g);
64+
for (const match of headerMatches) {
65+
const key = match[1];
66+
const value = match[2];
67+
if (key && value) {
68+
result.headers[key] = value;
69+
}
70+
}
71+
72+
const bodyMatch = curlCommand.match(/-d\s+'([^']+)'/);
73+
if (bodyMatch && bodyMatch[1]) {
74+
result.body = bodyMatch[1];
75+
}
76+
77+
const urlMatch = curlCommand.match(/"(https?:\/\/[^"]+)"/);
78+
if (urlMatch && urlMatch[1]) {
79+
result.url = urlMatch[1];
80+
}
81+
82+
return result;
83+
};

src/utils/filters.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { NetworkLog } from '../types';
2+
3+
export type StatusFilterKey =
4+
| 'all'
5+
| 'success'
6+
| 'redirect'
7+
| 'clientError'
8+
| 'serverError'
9+
| 'error';
10+
11+
export interface FilterState {
12+
searchTerm: string;
13+
statusFilter: StatusFilterKey;
14+
methodFilter: string | null;
15+
apiOnly: boolean;
16+
}
17+
18+
export const defaultFilterState: FilterState = {
19+
searchTerm: '',
20+
statusFilter: 'all',
21+
methodFilter: null,
22+
apiOnly: false,
23+
};
24+
25+
export const matchesSearchTerm = (
26+
log: NetworkLog,
27+
searchTerm: string
28+
): boolean => {
29+
if (!searchTerm) return true;
30+
const lowerSearch = searchTerm.toLowerCase();
31+
return (
32+
log.url.toLowerCase().includes(lowerSearch) ||
33+
log.method.toLowerCase().includes(lowerSearch) ||
34+
(log.response?.body?.toLowerCase().includes(lowerSearch) ?? false) ||
35+
(log.body?.toLowerCase().includes(lowerSearch) ?? false)
36+
);
37+
};
38+
39+
export const matchesStatusFilter = (
40+
log: NetworkLog,
41+
statusFilter: StatusFilterKey
42+
): boolean => {
43+
const status = log.response?.status;
44+
const hasError = !!log.error;
45+
46+
switch (statusFilter) {
47+
case 'all':
48+
return true;
49+
case 'success':
50+
return status !== undefined && status >= 200 && status < 300;
51+
case 'redirect':
52+
return status !== undefined && status >= 300 && status < 400;
53+
case 'clientError':
54+
return status !== undefined && status >= 400 && status < 500;
55+
case 'serverError':
56+
return status !== undefined && status >= 500;
57+
case 'error':
58+
return hasError || (status !== undefined && status >= 400);
59+
default:
60+
return true;
61+
}
62+
};
63+
64+
export const matchesMethodFilter = (
65+
log: NetworkLog,
66+
methodFilter: string | null
67+
): boolean => {
68+
if (!methodFilter) return true;
69+
return log.method.toUpperCase() === methodFilter.toUpperCase();
70+
};
71+
72+
export const matchesApiFilter = (
73+
log: NetworkLog,
74+
apiOnly: boolean
75+
): boolean => {
76+
if (!apiOnly) return true;
77+
return log.url.toLowerCase().includes('api');
78+
};
79+
80+
export const filterLogs = (
81+
logs: NetworkLog[],
82+
filters: FilterState
83+
): NetworkLog[] => {
84+
return logs.filter((log) => {
85+
return (
86+
matchesSearchTerm(log, filters.searchTerm) &&
87+
matchesStatusFilter(log, filters.statusFilter) &&
88+
matchesMethodFilter(log, filters.methodFilter) &&
89+
matchesApiFilter(log, filters.apiOnly)
90+
);
91+
});
92+
};
93+
94+
export const shouldIgnoreUrl = (url: string, patterns: string[]): boolean => {
95+
if (patterns.length === 0) return false;
96+
return patterns.some((pattern) => {
97+
if (pattern.includes('*')) {
98+
const regex = new RegExp(pattern.replace(/\*/g, '.*'), 'i');
99+
return regex.test(url);
100+
}
101+
return url.toLowerCase().includes(pattern.toLowerCase());
102+
});
103+
};
104+
105+
export const shouldIgnoreDomain = (url: string, domains: string[]): boolean => {
106+
if (domains.length === 0) return false;
107+
try {
108+
const urlObj = new URL(url);
109+
return domains.some(
110+
(domain) => urlObj.hostname.toLowerCase() === domain.toLowerCase()
111+
);
112+
} catch {
113+
return false;
114+
}
115+
};
116+
117+
export const getLogStats = (
118+
logs: NetworkLog[]
119+
): {
120+
total: number;
121+
success: number;
122+
errors: number;
123+
pending: number;
124+
avgDuration: number;
125+
} => {
126+
const stats = {
127+
total: logs.length,
128+
success: 0,
129+
errors: 0,
130+
pending: 0,
131+
avgDuration: 0,
132+
};
133+
134+
let totalDuration = 0;
135+
let durationCount = 0;
136+
137+
logs.forEach((log) => {
138+
const status = log.response?.status;
139+
const duration = log.response?.duration || log.duration;
140+
141+
if (log.error || (status !== undefined && status >= 400)) {
142+
stats.errors++;
143+
} else if (status !== undefined && status >= 200 && status < 400) {
144+
stats.success++;
145+
} else {
146+
stats.pending++;
147+
}
148+
149+
if (duration) {
150+
totalDuration += duration;
151+
durationCount++;
152+
}
153+
});
154+
155+
stats.avgDuration =
156+
durationCount > 0 ? Math.round(totalDuration / durationCount) : 0;
157+
158+
return stats;
159+
};

src/utils/formatters.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { NetworkRequestHeaders } from '../types';
2+
3+
export const formatHeaders = (headers?: NetworkRequestHeaders): string => {
4+
if (!headers || typeof headers !== 'object') return 'No headers';
5+
return Object.entries(headers)
6+
.map(([key, value]: [string, string]) => `${key}: ${value}`)
7+
.join('\n');
8+
};
9+
10+
export const formatBody = (body?: string | null): string => {
11+
if (!body) return 'No body';
12+
try {
13+
const parsed = JSON.parse(body);
14+
return JSON.stringify(parsed, null, 2);
15+
} catch {
16+
return body;
17+
}
18+
};
19+
20+
export const formatTimestamp = (timestamp: string): string => {
21+
const date = new Date(timestamp);
22+
return date.toLocaleTimeString('en-US', {
23+
hour: '2-digit',
24+
minute: '2-digit',
25+
second: '2-digit',
26+
hour12: false,
27+
});
28+
};
29+
30+
export const formatDuration = (ms: number): string => {
31+
if (ms < 1000) return `${ms}ms`;
32+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
33+
return `${(ms / 60000).toFixed(2)}m`;
34+
};
35+
36+
export const formatBytes = (bytes: number): string => {
37+
if (bytes === 0) return '0 B';
38+
const k = 1024;
39+
const sizes = ['B', 'KB', 'MB', 'GB'];
40+
const i = Math.floor(Math.log(bytes) / Math.log(k));
41+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
42+
};
43+
44+
export const truncateUrl = (url: string, maxLength: number = 50): string => {
45+
if (url.length <= maxLength) return url;
46+
return `${url.substring(0, maxLength)}...`;
47+
};
48+
49+
export const extractDomain = (url: string): string => {
50+
try {
51+
const urlObj = new URL(url);
52+
return urlObj.hostname;
53+
} catch {
54+
return url;
55+
}
56+
};
57+
58+
export const extractPath = (url: string): string => {
59+
try {
60+
const urlObj = new URL(url);
61+
return urlObj.pathname + urlObj.search;
62+
} catch {
63+
return url;
64+
}
65+
};

src/utils/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from './formatters';
2+
export * from './filters';
3+
export * from './curl';
4+
export * from './export';
5+
export * from './replay';
6+
export * from './redact';

0 commit comments

Comments
 (0)