Created
October 12, 2025 03:32
-
-
Save discountry/08e69f9444942b5349f72b03387e2861 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| bun add puppeteer-core |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| }); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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