Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cli/src/lib/driver/commands/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { DriverCommandHandlers } from "./types.js";
export const networkHandlers: DriverCommandHandlers = {
async "network.on"(manager) {
const page = await manager.activePage();
return manager.network.enable(page);
const websocketUrl = await manager.networkWebSocketDebuggerUrl();
return manager.network.enable(page, websocketUrl);
},

async "network.off"(manager) {
Expand Down
105 changes: 60 additions & 45 deletions packages/cli/src/lib/driver/network-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
getNetworkDir,
writePrivateFile,
} from "./daemon/paths.js";
import { DriverError } from "./errors.js";
import {
NetworkCdpSidecar,
type NetworkCdpSession,
} from "./network-cdp-sidecar.js";

interface PendingRequest {
body: string | null;
Expand All @@ -26,37 +29,40 @@ interface ResponseMetadata {
statusText: string;
}

type CdpSession = {
off?: (event: string, listener: (...args: unknown[]) => void) => void;
on: (event: string, listener: (...args: unknown[]) => void) => void;
send: <T = unknown>(
method: string,
params?: Record<string, unknown>,
) => Promise<T>;
type StagehandV4Page = {
pageId: string;
};

/**
* The V3 Browse network writer, adapted only at the CDP-session boundary.
* Keeping request correlation and the on-disk request/response schema here
* unchanged gives the V4 CLI observable parity without adding a public
* Stagehand network-event API.
*/
export class NetworkCapture {
private cdpSession: CdpSession | null = null;
private cdpSession: NetworkCdpSession | null = null;
private counter = 0;
private enabled = false;
private readonly pendingRequests = new Map<string, PendingRequest>();
private readonly requestDirs = new Map<string, Promise<string | null>>();
private readonly requestStartTimes = new Map<string, number>();
private readonly responseMetadata = new Map<string, ResponseMetadata>();
private readonly listeners: Array<[string, (...args: unknown[]) => void]> =
[];
private readonly listeners: Array<[string, (params: unknown) => void]> = [];
private networkDir: string | null = null;

constructor(private readonly session: string) {}
constructor(
private readonly session: string,
private readonly sidecar = new NetworkCdpSidecar(),
) {}

async enable(
page: unknown,
page: StagehandV4Page,
browserWebSocketDebuggerUrl: string,
): Promise<{ alreadyEnabled?: boolean; enabled: true; path: string }> {
if (this.enabled && this.networkDir) {
if (this.enabled && this.networkDir && this.cdpSession?.connected) {
return { alreadyEnabled: true, enabled: true, path: this.networkDir };
}

const cdpSession = await this.networkCdpSession(page);
if (this.enabled) await this.disable();

await ensureRuntimeDir();
this.networkDir = getNetworkDir(this.session);
Expand All @@ -67,24 +73,34 @@ export class NetworkCapture {
this.requestStartTimes.clear();
this.responseMetadata.clear();

const cdpSession = await this.sidecar.attach(
browserWebSocketDebuggerUrl,
page.pageId,
);
this.cdpSession = cdpSession;
await cdpSession.send("Network.enable", {
maxResourceBufferSize: 5_000_000,
maxTotalBufferSize: 10_000_000,
});
try {
await cdpSession.send("Network.enable", {
maxResourceBufferSize: 5_000_000,
maxTotalBufferSize: 10_000_000,
});

this.addListener("Network.requestWillBeSent", (params) => {
void this.handleRequestWillBeSent(params);
});
this.addListener("Network.responseReceived", (params) => {
this.handleResponseReceived(params);
});
this.addListener("Network.loadingFinished", (params) => {
void this.handleLoadingFinished(params);
});
this.addListener("Network.loadingFailed", (params) => {
void this.handleLoadingFailed(params);
});
this.addListener("Network.requestWillBeSent", (params) => {
void this.handleRequestWillBeSent(params);
});
this.addListener("Network.responseReceived", (params) => {
this.handleResponseReceived(params);
});
this.addListener("Network.loadingFinished", (params) => {
void this.handleLoadingFinished(params);
});
this.addListener("Network.loadingFailed", (params) => {
void this.handleLoadingFailed(params);
});
} catch (error) {
this.cdpSession = null;
await cdpSession.detach().catch(() => undefined);
throw error;
}

this.enabled = true;
return { enabled: true, path: this.networkDir };
Expand All @@ -99,20 +115,27 @@ export class NetworkCapture {
return { alreadyDisabled: true, enabled: false, path: this.networkDir };
}

const cdpSession = this.cdpSession;
this.enabled = false;
for (const [event, listener] of this.listeners) {
this.cdpSession?.off?.(event, listener);
cdpSession?.off(event, listener);
}
this.listeners.length = 0;

await this.cdpSession?.send("Network.disable").catch(() => undefined);
await cdpSession?.send("Network.disable").catch(() => undefined);
await cdpSession?.detach().catch(() => undefined);
this.cdpSession = null;
this.enabled = false;
return { enabled: false, path: this.networkDir };
}

async close(): Promise<void> {
await this.disable().catch(() => undefined);
this.sidecar.close();
}

path(): { enabled: boolean; path: string } {
return {
enabled: this.enabled,
enabled: this.enabled && (this.cdpSession?.connected ?? false),
path: this.networkDir ?? getNetworkDir(this.session),
};
}
Expand Down Expand Up @@ -146,20 +169,12 @@ export class NetworkCapture {

private addListener(
event: string,
listener: (...args: unknown[]) => void,
listener: (params: unknown) => void,
): void {
this.cdpSession?.on(event, listener);
this.listeners.push([event, listener]);
}

private async networkCdpSession(page: unknown): Promise<CdpSession> {
void page;
throw new DriverError(
"Network capture is not available in this Stagehand V4 runtime. Apply the CLI CDP sidecar fast-follow to restore `browse network on`.",
{ code: "network_capture_unavailable" },
);
}

private handleRequestWillBeSent(params: unknown): void {
if (!this.enabled || !this.networkDir) return;
const event = params as {
Expand Down
Loading
Loading