chromancer
Version:
A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.
289 lines (288 loc) ⢠11.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const base_js_1 = require("../base.js");
const fs_1 = require("fs");
class Network extends base_js_1.BaseCommand {
static description = 'Advanced network monitoring with request filtering, response capture, performance analysis, and request blocking - debug API calls in real-time';
static examples = [
'<%= config.bin %> <%= command.id %> --filter "api"',
'<%= config.bin %> <%= command.id %> --type xhr --output api-calls.json',
'<%= config.bin %> <%= command.id %> --method POST --verbose',
'<%= config.bin %> <%= command.id %> --status 404 --status 500',
'<%= config.bin %> <%= command.id %> --block "ads" --block "analytics"',
];
static flags = {
...base_js_1.BaseCommand.baseFlags,
filter: core_1.Flags.string({
char: 'f',
description: 'URL filter pattern',
multiple: true,
}),
type: core_1.Flags.string({
char: 't',
description: 'Resource type filter',
options: ['document', 'stylesheet', 'image', 'media', 'font', 'script', 'xhr', 'fetch', 'websocket'],
multiple: true,
}),
method: core_1.Flags.string({
char: 'm',
description: 'HTTP method filter',
options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
multiple: true,
}),
status: core_1.Flags.integer({
description: 'HTTP status code filter',
multiple: true,
}),
block: core_1.Flags.string({
char: 'b',
description: 'Block requests matching pattern',
multiple: true,
}),
output: core_1.Flags.string({
char: 'o',
description: 'Output file for network log',
}),
'capture-response': core_1.Flags.boolean({
description: 'Capture response bodies',
default: false,
}),
'live-mode': core_1.Flags.boolean({
description: 'Show requests in real-time',
default: true,
}),
duration: core_1.Flags.integer({
char: 'd',
description: 'Monitor duration in milliseconds',
default: 60000, // 1 minute
}),
};
networkLogs = [];
requestTimings = new Map();
async run() {
const { flags } = await this.parse(Network);
await this.connectToChrome(flags.port, flags.host, flags.launch, flags.profile, flags.headless, flags.verbose, true // Always keep open for monitoring
);
if (!this.page) {
this.error('Failed to connect to Chrome');
}
await this.monitorNetwork(this.page);
}
async monitorNetwork(page) {
const { flags } = await this.parse(Network);
this.log('š Network monitoring started...');
this.log(`Press Ctrl+C to stop (max duration: ${flags.duration}ms)\n`);
// Set up request interception if blocking
if (flags.block && flags.block.length > 0) {
await page.route('**/*', async (route) => {
const url = route.request().url();
const shouldBlock = flags.block.some(pattern => url.includes(pattern));
if (shouldBlock) {
await route.abort();
if (flags['live-mode']) {
this.log(`š« Blocked: ${url}`);
}
}
else {
await route.continue();
}
});
}
// Monitor requests
page.on('request', (request) => {
this.requestTimings.set(request.url(), Date.now());
if (this.shouldLogRequest(request)) {
const log = {
timestamp: new Date().toISOString(),
method: request.method(),
url: request.url(),
type: request.resourceType(),
};
if (flags.verbose || flags['capture-response']) {
log.headers = request.headers();
log.postData = request.postData() || undefined;
}
if (flags['live-mode']) {
this.log(`ā ${request.method()} ${request.url()}`);
}
this.networkLogs.push(log);
}
});
// Monitor responses
page.on('response', async (response) => {
const request = response.request();
if (this.shouldLogRequest(request) && this.shouldLogResponse(response)) {
const startTime = this.requestTimings.get(request.url());
const duration = startTime ? Date.now() - startTime : undefined;
// Find and update the request log
const logIndex = this.networkLogs.findIndex(log => log.url === request.url() && log.method === request.method() && !log.status);
if (logIndex !== -1) {
this.networkLogs[logIndex].status = response.status();
this.networkLogs[logIndex].duration = duration;
try {
const headers = await response.allHeaders();
const size = headers['content-length'] ? parseInt(headers['content-length']) : undefined;
this.networkLogs[logIndex].size = size;
if (flags['capture-response']) {
try {
const contentType = headers['content-type'] || '';
if (contentType.includes('json') || contentType.includes('text')) {
this.networkLogs[logIndex].response = await response.text();
}
}
catch {
// Ignore response body errors
}
}
}
catch {
// Ignore header errors
}
}
if (flags['live-mode']) {
const status = response.status();
const statusIcon = status >= 200 && status < 300 ? 'ā' : status >= 400 ? 'ā' : 'ā¢';
const sizeStr = this.networkLogs[logIndex]?.size
? ` (${this.formatBytes(this.networkLogs[logIndex].size)})`
: '';
const durationStr = duration ? ` ${duration}ms` : '';
this.log(`ā ${statusIcon} ${status} ${request.url()}${sizeStr}${durationStr}`);
}
}
});
// Monitor failed requests
page.on('requestfailed', (request) => {
if (this.shouldLogRequest(request)) {
if (flags['live-mode']) {
this.log(`ā Failed: ${request.method()} ${request.url()}`);
}
const logIndex = this.networkLogs.findIndex(log => log.url === request.url() && log.method === request.method() && !log.status);
if (logIndex !== -1) {
this.networkLogs[logIndex].status = 0; // Failed request
}
}
});
// Set up monitoring duration
const timeout = setTimeout(() => {
this.stopMonitoring();
}, flags.duration);
// Handle Ctrl+C
process.once('SIGINT', () => {
clearTimeout(timeout);
this.stopMonitoring();
});
}
shouldLogRequest(request) {
const { flags } = this.parsedFlags;
// Filter by URL pattern
if (flags.filter && flags.filter.length > 0) {
const url = request.url();
if (!flags.filter.some((pattern) => url.includes(pattern))) {
return false;
}
}
// Filter by resource type
if (flags.type && flags.type.length > 0) {
if (!flags.type.includes(request.resourceType())) {
return false;
}
}
// Filter by method
if (flags.method && flags.method.length > 0) {
if (!flags.method.includes(request.method())) {
return false;
}
}
return true;
}
shouldLogResponse(response) {
const { flags } = this.parsedFlags;
// Filter by status code
if (flags.status && flags.status.length > 0) {
if (!flags.status.includes(response.status())) {
return false;
}
}
return true;
}
async stopMonitoring() {
const { flags } = this.parsedFlags;
this.log('\nā¹ļø Network monitoring stopped');
this.log(`Captured ${this.networkLogs.length} requests`);
// Summary statistics
const stats = this.calculateStats();
this.log('\nš Summary:');
this.log(` Total requests: ${stats.total}`);
this.log(` Successful: ${stats.successful} (2xx)`);
this.log(` Redirects: ${stats.redirects} (3xx)`);
this.log(` Client errors: ${stats.clientErrors} (4xx)`);
this.log(` Server errors: ${stats.serverErrors} (5xx)`);
this.log(` Failed: ${stats.failed}`);
if (stats.totalSize > 0) {
this.log(` Total size: ${this.formatBytes(stats.totalSize)}`);
}
if (stats.avgDuration > 0) {
this.log(` Average duration: ${stats.avgDuration.toFixed(0)}ms`);
}
// Save to file if requested
if (flags.output) {
await fs_1.promises.writeFile(flags.output, JSON.stringify(this.networkLogs, null, 2));
this.log(`\nā
Network log saved to: ${flags.output}`);
}
process.exit(0);
}
calculateStats() {
const stats = {
total: this.networkLogs.length,
successful: 0,
redirects: 0,
clientErrors: 0,
serverErrors: 0,
failed: 0,
totalSize: 0,
totalDuration: 0,
avgDuration: 0,
};
for (const log of this.networkLogs) {
if (log.status === 0) {
stats.failed++;
}
else if (log.status && log.status >= 200 && log.status < 300) {
stats.successful++;
}
else if (log.status && log.status >= 300 && log.status < 400) {
stats.redirects++;
}
else if (log.status && log.status >= 400 && log.status < 500) {
stats.clientErrors++;
}
else if (log.status && log.status >= 500) {
stats.serverErrors++;
}
if (log.size) {
stats.totalSize += log.size;
}
if (log.duration) {
stats.totalDuration += log.duration;
}
}
const completedRequests = this.networkLogs.filter(log => log.duration).length;
if (completedRequests > 0) {
stats.avgDuration = stats.totalDuration / completedRequests;
}
return stats;
}
formatBytes(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
}
get parsedFlags() {
return this.parse(Network).then(result => result.flags);
}
}
exports.default = Network;