Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:05:10 +08:00
commit 1c87823db1
17 changed files with 1920 additions and 0 deletions

254
skills/scripts/network.ts Normal file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* Network request debugging and performance analysis
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--duration 5000] [--output requests.json]
*/
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js';
import { writeFile } from 'fs/promises';
interface NetworkArgs {
url?: string;
types?: string;
duration?: string;
output?: string;
timeout?: string;
headless?: string | boolean;
close?: string | boolean;
browser?: string;
}
interface NetworkRequest {
url: string;
method: string;
status: number;
statusText: string;
type: string;
resourceType: string;
size: {
request: number;
response: number;
total: number;
};
timing: {
startTime: number;
endTime: number;
duration: number;
};
headers: {
request: Record<string, string>;
response: Record<string, string>;
};
failed: boolean;
errorText?: string;
}
async function monitorNetwork() {
const args = parseArgs(process.argv.slice(2)) as NetworkArgs;
if (!args.url) {
return outputError(new Error('--url is required'));
return;
}
try {
const session = await getBrowserSession({
browser: args.browser as any,
headless: args.headless !== 'false',
timeout: args.timeout ? parseInt(args.timeout) : undefined,
});
const requests: NetworkRequest[] = [];
const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null;
const duration = args.duration ? parseInt(args.duration) : 5000;
// Set up request and response listeners
session.page.on('request', (request) => {
const resourceType = request.resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const requestData: any = {
url: request.url(),
method: request.method(),
resourceType: resourceType,
type: 'request',
headers: request.headers(),
size: {
request: 0, // Playwright doesn't expose header size directly
response: 0,
total: 0,
},
timing: {
startTime: Date.now(),
endTime: 0,
duration: 0,
},
status: 0,
statusText: '',
failed: false,
};
// Store temporary request data
(request as any)._requestData = requestData;
});
session.page.on('response', async (response) => {
const request = response.request() as any;
const requestData = request._requestData;
if (!requestData) {
return; // Skip if not tracked
}
const resourceType = response.request().resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const endTime = Date.now();
const responseBody = await response.text();
const responseSize = responseBody.length;
const networkRequest: NetworkRequest = {
url: response.url(),
method: request.method(),
status: response.status(),
statusText: response.statusText(),
type: 'response',
resourceType: resourceType,
size: {
request: requestData.size.request,
response: responseSize,
total: requestData.size.request + responseSize,
},
timing: {
startTime: requestData.timing.startTime,
endTime: endTime,
duration: endTime - requestData.timing.startTime,
},
headers: {
request: requestData.headers,
response: response.headers(),
},
failed: !response.ok(),
errorText: !response.ok() ? response.statusText() : undefined,
};
requests.push(networkRequest);
});
session.page.on('requestfailed', (request) => {
const requestData = (request as any)._requestData;
if (!requestData) {
return; // Skip if not tracked
}
const resourceType = request.resourceType().toLowerCase();
// Filter by resource types if specified
if (filterTypes && !filterTypes.includes(resourceType)) {
return;
}
const endTime = Date.now();
const networkRequest: NetworkRequest = {
url: request.url(),
method: request.method(),
status: 0,
statusText: 'Failed',
type: 'failed',
resourceType: resourceType,
size: requestData.size,
timing: {
startTime: requestData.timing.startTime,
endTime: endTime,
duration: endTime - requestData.timing.startTime,
},
headers: {
request: requestData.headers,
response: {},
},
failed: true,
errorText: (request as any).failure()?.errorText || 'Request failed',
};
requests.push(networkRequest);
});
// Navigate to the URL
await session.page.goto(args.url, { waitUntil: 'networkidle' });
// Wait for the specified duration
await new Promise(resolve => setTimeout(resolve, duration));
// Calculate summary statistics
const summary = {
totalRequests: requests.length,
successfulRequests: requests.filter(r => !r.failed).length,
failedRequests: requests.filter(r => r.failed).length,
totalSize: requests.reduce((sum, r) => sum + r.size.total, 0),
averageResponseTime: requests.length > 0
? requests.reduce((sum, r) => sum + r.timing.duration, 0) / requests.length
: 0,
requestsByType: requests.reduce((acc, r) => {
acc[r.resourceType] = (acc[r.resourceType] || 0) + 1;
return acc;
}, {} as Record<string, number>),
requestsByStatus: requests.reduce((acc, r) => {
if (r.failed) {
acc['failed'] = (acc['failed'] || 0) + 1;
} else {
const statusGroup = Math.floor(r.status / 100) * 100;
acc[statusGroup] = (acc[statusGroup] || 0) + 1;
}
return acc;
}, {} as Record<string, number>),
};
const result = {
success: true,
url: session.page.url(),
title: await session.page.title(),
monitoring: {
duration: duration,
types: filterTypes || 'all',
...summary,
},
requests: requests,
sessionId: session.sessionId,
timestamp: new Date().toISOString(),
};
// Save to file if output path provided
if (args.output) {
await writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
...summary,
url: session.page.url()
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowserSession();
} else {
// Explicitly exit the process when keeping session open
process.exit(0);
}
} catch (error) {
return outputError(error instanceof Error ? error : new Error(String(error)));
}
}
monitorNetwork();