Skip to content

Instantly share code, notes, and snippets.

@discountry
Created October 12, 2025 03:32
Show Gist options
  • Select an option

  • Save discountry/08e69f9444942b5349f72b03387e2861 to your computer and use it in GitHub Desktop.

Select an option

Save discountry/08e69f9444942b5349f72b03387e2861 to your computer and use it in GitHub Desktop.
bun add puppeteer-core
import puppeteer from "puppeteer-core";
import type { Browser, Page } from "puppeteer-core";
// Minimal standalone network watcher for any page in an already-running Chrome (remote debugging)
// No external dependencies besides puppeteer-core. Suitable for publishing as a single-file gist.
type HeaderMap = Record<string, string>;
type RequestRecord = {
requestId: string;
url: string;
method: string;
resourceType?: string;
requestHeaders: HeaderMap;
requestBodyText?: string;
responseStatus?: number;
responseMimeType?: string;
responseHeaders?: HeaderMap;
fromDiskCache?: boolean;
fromServiceWorker?: boolean;
tsStart: number;
tsEnd?: number;
};
const BROWSER_URL = process.env.REMOTE_DEBUGGING_URL || "http://127.0.0.1:9222";
const MAX_BODY_PREVIEW_CHARS = Number(process.env.MAX_BODY_PREVIEW_CHARS ?? 4000);
function normalizeHeaders(input: Record<string, unknown> | undefined | null): HeaderMap {
const out: HeaderMap = {};
if (!input) return out;
for (const [k, v] of Object.entries(input)) {
try { out[String(k).toLowerCase()] = typeof v === "string" ? v : JSON.stringify(v); } catch {}
}
return out;
}
function isProbablyTextual(contentType: string | undefined): boolean {
if (!contentType) return false;
const ct = contentType.toLowerCase();
return ct.startsWith("text/") ||
ct.includes("application/json") ||
ct.includes("application/javascript") ||
ct.includes("application/xml") ||
ct.includes("application/x-www-form-urlencoded");
}
function truncate(text: string, max: number): string {
if (text.length <= max) return text;
return text.slice(0, Math.max(0, max - 20)) + `... [truncated ${text.length - max} chars]`;
}
function safeJsonParse(text: string): unknown | null {
try { return JSON.parse(text); } catch { return null; }
}
function section(title: string) {
const line = "=".repeat(Math.max(8, Math.min(120, title.length + 8)));
console.log("\n" + line);
console.log(title);
console.log(line);
}
function box(title: string, data: Record<string, unknown>) {
console.log(`\n[${title}]`);
for (const [k, v] of Object.entries(data)) {
const val = typeof v === "string" ? v : JSON.stringify(v, null, 2);
console.log(`- ${k}: ${val}`);
}
}
function jblock(title: string, obj: unknown) {
console.log(`\n[${title}]`);
try { console.log(JSON.stringify(obj, null, 2)); }
catch { console.log(String(obj)); }
}
async function logHttpCompletion(client: any, record: RequestRecord) {
let bodyText: string | null = null;
let base64Encoded = false;
try {
const body = await client.send("Network.getResponseBody", { requestId: record.requestId });
bodyText = typeof body?.body === "string" ? body.body : null;
base64Encoded = !!body?.base64Encoded;
} catch {}
const contentType = (record.responseHeaders?.["content-type"]) || record.responseMimeType || "";
const textual = isProbablyTextual(contentType);
let displayText: string | null = null;
if (bodyText != null) {
if (base64Encoded) {
try {
const buf = Buffer.from(bodyText, "base64");
displayText = textual ? buf.toString("utf8") : `[binary ${buf.length} bytes]`;
} catch {
displayText = `[base64 decode failed, ${bodyText.length} chars]`;
}
} else {
displayText = textual ? bodyText : `[binary/text ${bodyText.length} chars]`;
}
}
const durationMs = (record.tsEnd && record.tsEnd >= record.tsStart) ? (record.tsEnd - record.tsStart) : undefined;
section(`${record.method} ${record.url}`);
box("Summary", {
method: record.method,
url: record.url,
status: String(record.responseStatus ?? ""),
resourceType: record.resourceType || "",
mimeType: contentType || "",
durationMs: String(durationMs ?? "")
});
if (Object.keys(record.requestHeaders || {}).length) {
jblock("Request Headers", record.requestHeaders);
}
if (record.requestBodyText && record.requestBodyText.trim()) {
const parsed = safeJsonParse(record.requestBodyText);
if (parsed != null) jblock("Request Body (JSON)", parsed);
else box("Request Body (text)", { preview: truncate(record.requestBodyText, MAX_BODY_PREVIEW_CHARS) });
}
if (record.responseHeaders && Object.keys(record.responseHeaders).length) {
jblock("Response Headers", record.responseHeaders);
}
if (displayText != null && textual) {
const parsed = safeJsonParse(displayText);
if (parsed != null) jblock("Response Body (JSON)", parsed);
else box("Response Body (text)", { preview: truncate(displayText, MAX_BODY_PREVIEW_CHARS) });
} else if (displayText) {
box("Response Body", { note: displayText });
} else {
box("Response Body", { note: "<unavailable>" });
}
}
async function setupNetworkLoggingForPage(page: Page) {
const client = await page.target().createCDPSession();
await client.send("Network.enable", {});
const reqMap = new Map<string, RequestRecord>();
const pageLabel = () => {
try { return page.url() || "about:blank"; } catch { return "<closed>"; }
};
console.log(`Attached Network observer to page: ${pageLabel()}`);
client.on("Network.requestWillBeSent", (e: any) => {
const id = e.requestId as string;
const rec: RequestRecord = reqMap.get(id) || {
requestId: id,
url: e.request?.url || "",
method: e.request?.method || "",
resourceType: e.type || "",
requestHeaders: normalizeHeaders(e.request?.headers),
requestBodyText: typeof e.request?.postData === "string" ? e.request.postData : undefined,
tsStart: Date.now(),
};
rec.url = e.request?.url || rec.url;
rec.method = e.request?.method || rec.method;
rec.resourceType = e.type || rec.resourceType;
rec.requestHeaders = { ...rec.requestHeaders, ...normalizeHeaders(e.request?.headers) };
if (!rec.requestBodyText && typeof e.request?.postData === "string") rec.requestBodyText = e.request.postData;
reqMap.set(id, rec);
});
client.on("Network.requestWillBeSentExtraInfo", (e: any) => {
const id = e.requestId as string;
const rec = reqMap.get(id) || {
requestId: id,
url: "",
method: "",
requestHeaders: {},
tsStart: Date.now(),
} as RequestRecord;
rec.requestHeaders = { ...rec.requestHeaders, ...normalizeHeaders(e.headers) };
reqMap.set(id, rec);
});
client.on("Network.responseReceived", (e: any) => {
const id = e.requestId as string;
const rec = reqMap.get(id) || {
requestId: id,
url: e.response?.url || "",
method: "",
requestHeaders: {},
tsStart: Date.now(),
} as RequestRecord;
rec.responseStatus = e.response?.status;
rec.responseMimeType = e.response?.mimeType;
rec.responseHeaders = { ...normalizeHeaders(e.response?.headers), ...(rec.responseHeaders || {}) };
rec.fromDiskCache = !!e.response?.fromDiskCache;
rec.fromServiceWorker = !!e.response?.fromServiceWorker;
reqMap.set(id, rec);
});
client.on("Network.responseReceivedExtraInfo", (e: any) => {
const id = e.requestId as string;
const rec = reqMap.get(id) || {
requestId: id,
url: "",
method: "",
requestHeaders: {},
tsStart: Date.now(),
} as RequestRecord;
rec.responseHeaders = { ...(rec.responseHeaders || {}), ...normalizeHeaders(e.headers) };
reqMap.set(id, rec);
});
client.on("Network.loadingFinished", async (e: any) => {
const id = e.requestId as string;
const rec = reqMap.get(id);
if (!rec) return;
rec.tsEnd = Date.now();
try { await logHttpCompletion(client, rec); } catch (err) { console.error("Log completion failed", err); }
finally { reqMap.delete(id); }
});
client.on("Network.loadingFailed", (e: any) => {
const id = e.requestId as string;
const rec = reqMap.get(id);
const url = rec?.url || e?.request?.url || "";
section(`FAILED ${rec?.method || ""} ${url}`);
box("Failure", {
errorText: String(e.errorText || ""),
canceled: String(!!e.canceled),
});
if (rec?.requestHeaders) jblock("Request Headers", rec.requestHeaders);
reqMap.delete(id);
});
// Optional: log WebSocket handshakes (not frames to avoid noise)
client.on("Network.webSocketCreated", (e: any) => {
section(`WebSocket ${e.url}`);
});
client.on("Network.webSocketHandshakeRequestSent", (e: any) => {
box("WS Handshake Request", { url: e.request?.url || "" });
if (e.request?.headers) jblock("WS Request Headers", normalizeHeaders(e.request.headers));
});
client.on("Network.webSocketHandshakeResponseReceived", (e: any) => {
box("WS Handshake Response", { status: String(e.response?.status || "") });
if (e.response?.headers) jblock("WS Response Headers", normalizeHeaders(e.response.headers));
});
page.on("close", async () => {
try { await client.detach?.(); } catch {}
});
}
async function attachAllExistingPages(browser: Browser) {
const pages = await browser.pages();
for (const p of pages) {
try { await setupNetworkLoggingForPage(p); } catch (e) { console.error("Attach failed", e); }
}
}
async function main() {
section("Standalone Network Watcher");
console.log(`Connecting to Chrome at ${BROWSER_URL}`);
const browser = await puppeteer.connect({ browserURL: BROWSER_URL, defaultViewport: null });
await attachAllExistingPages(browser);
browser.on("targetcreated", async (target) => {
try {
if (target.type() !== "page") return;
const p = await target.page();
if (!p) return;
await setupNetworkLoggingForPage(p);
} catch (e) {
console.error("targetcreated handler error", e);
}
});
const shutdown = async () => {
try {
const pages = await browser.pages();
for (const p of pages) {
try { const c = await p.target().createCDPSession(); await c.detach?.(); } catch {}
}
} catch {}
try { await browser.disconnect(); } catch {}
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
main().catch(err => {
console.error("Fatal", err);
process.exit(1);
});
@echo off
REM 启动已登录的 Chrome 并开启远程调试端口 9222
set "ChromeExe=C:\Program Files\Google\Chrome\Application\chrome.exe"
set "ProfileDir=C:\Users\%USERNAME%\chrome-example-profile"
start "" "%ChromeExe%" --remote-debugging-port=9222 --user-data-dir="%ProfileDir%" "https://example.com"
open -n -a "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/chrome-example-profile" "https://example.com"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment