browse
Version:
Unified Browserbase CLI for browser automation and cloud APIs.
183 lines (182 loc) • 6.36 kB
JavaScript
import WebSocket from "ws";
import { resolveWsTarget } from "./resolve-ws.js";
export const DEFAULT_CDP_DOMAINS = [
"Network",
"Console",
"Runtime",
"Log",
"Page",
];
export async function tailCdp(target, options = {}) {
const wsUrl = await resolveWsTarget(target);
const domains = options.domains?.length
? options.domains
: DEFAULT_CDP_DOMAINS;
const usePretty = options.pretty ?? false;
let messageId = 1;
const pendingIds = new Set();
const targetSessionMap = new Map();
function send(ws, method, params = {}, sessionId) {
const id = messageId++;
pendingIds.add(id);
const message = { id, method, params };
if (sessionId)
message.sessionId = sessionId;
ws.send(JSON.stringify(message));
return id;
}
function enableDomains(ws, sessionId) {
for (const domain of domains) {
if (domain === "Network") {
send(ws, "Network.enable", { maxResourceBufferSize: 100_000, maxTotalBufferSize: 1_000_000 }, sessionId);
}
else {
send(ws, `${domain}.enable`, {}, sessionId);
}
if (domain === "Page") {
send(ws, "Page.setLifecycleEventsEnabled", { enabled: true }, sessionId);
}
}
}
await new Promise((resolve) => {
const ws = new WebSocket(wsUrl);
let closed = false;
const cleanup = () => {
if (closed)
return;
closed = true;
if (ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
resolve();
};
process.once("SIGINT", cleanup);
process.once("SIGTERM", cleanup);
ws.on("open", () => {
if (usePretty) {
process.stderr.write(`Connected to ${wsUrl}\n`);
}
send(ws, "Target.setAutoAttach", {
autoAttach: true,
filter: [{ type: "page" }],
flatten: true,
waitForDebuggerOnStart: false,
});
send(ws, "Target.setDiscoverTargets", {
discover: true,
filter: [{ type: "page" }],
});
});
ws.on("message", (raw) => {
let message;
try {
message = JSON.parse(raw.toString());
}
catch {
return;
}
if (message.id !== undefined && pendingIds.has(message.id)) {
pendingIds.delete(message.id);
if (message.error) {
process.stderr.write(`CDP error (id=${message.id}): ${message.error.message}\n`);
}
return;
}
if (message.method === "Target.attachedToTarget" && message.params) {
const params = message.params;
if (params.sessionId &&
params.targetInfo?.type === "page" &&
params.targetInfo.targetId) {
targetSessionMap.set(params.targetInfo.targetId, params.sessionId);
enableDomains(ws, params.sessionId);
}
}
if (message.method === "Target.detachedFromTarget" && message.params) {
const params = message.params;
const targetId = params.targetId ??
[...targetSessionMap.entries()].find(([, sessionId]) => sessionId === params.sessionId)?.[0];
if (targetId)
targetSessionMap.delete(targetId);
}
writeCdpMessage(message, usePretty);
});
ws.on("error", (error) => {
process.stderr.write(`Error: ${error.message}\n`);
});
ws.on("close", () => {
if (!closed && usePretty) {
process.stderr.write("Disconnected.\n");
}
cleanup();
});
});
}
function writeCdpMessage(message, pretty) {
if (!pretty) {
writeLine(JSON.stringify(message));
return;
}
const line = formatPrettyCdpMessage(message);
if (line)
writeLine(line);
}
function writeLine(line) {
try {
process.stdout.write(`${line}\n`);
}
catch (error) {
if (error.code === "EPIPE")
process.exit(0);
throw error;
}
}
function formatPrettyCdpMessage(message) {
if (!message.method)
return null;
const params = message.params;
let line = `[${message.method}]`;
try {
if (message.method === "Network.requestWillBeSent") {
const request = params?.request;
if (request)
line += ` ${request.method ?? "?"} ${request.url ?? ""}`;
}
else if (message.method === "Network.responseReceived") {
const response = params?.response;
if (response)
line += ` ${response.status ?? "?"} ${response.url ?? ""}`;
}
else if (message.method === "Network.loadingFailed") {
line += ` ${params?.errorText ?? "Unknown error"}`;
}
else if (message.method === "Runtime.consoleAPICalled") {
const type = params?.type ?? "log";
const args = params?.args ?? [];
line += ` [${type}] ${args.map((arg) => arg.description ?? arg.value ?? "").join(" ")}`;
}
else if (message.method === "Runtime.exceptionThrown") {
const detail = params?.exceptionDetails;
line += ` ${detail?.exception?.description ?? detail?.text ?? "Unknown exception"}`;
}
else if (message.method === "Page.frameNavigated") {
const url = params?.frame?.url;
if (url)
line += ` ${url}`;
}
else if (message.method === "Page.lifecycleEvent") {
const name = params?.name;
if (name)
line += ` ${name}`;
}
else if (message.method === "Target.attachedToTarget") {
const info = params?.targetInfo;
if (info)
line += ` [${info.type ?? "?"}] ${info.url ?? ""}`;
}
}
catch {
return `[${message.method}]`;
}
return line;
}