Repository: browserbase/stagehand
File: packages/core/lib/v3/understudy/cdp.ts (TypeScript)

Code before:
      // Reason is a Buffer in ws; stringify defensively
      const why = `socket-close code=${code} reason=${String(reason || "")}`;
      this.emitTransportClosed(why);
    });

    this.ws.on("error", (err) => {
      const why = `socket-error ${err?.message ?? String(err)}`;
      this.emitTransportClosed(why);
    });
    this.ws.on("message", (data) => this.onMessage(data.toString()));
  }

  static async connect(wsUrl: string): Promise<CdpConnection> {
    const ws = new WebSocket(wsUrl);
    await new Promise<void>((resolve, reject) => {
      ws.once("open", () => resolve());
      ws.once("error", (e) => reject(e));
    });
    return new CdpConnection(ws);
  }

  async enableAutoAttach(): Promise<void> {
    await this.send("Target.setAutoAttach", {
      autoAttach: true,
      flatten: true,
      waitForDebuggerOnStart: false,
      filter: [
        { type: "worker", exclude: true },
        { type: "shared_worker", exclude: true },
        { type: "service_worker", exclude: true },
      ],
    });
    await this.send("Target.setDiscoverTargets", { discover: true });
  }

  async send<R = unknown>(method: string, params?: object): Promise<R> {
    const id = this.nextId++;
    const payload = { id, method, params };
    const stack = new Error().stack?.split("\n").slice(1, 4).join("\n");
    const p = new Promise<R>((resolve, reject) => {
      this.inflight.set(id, {
        resolve,
        reject,
        sessionId: null,
        method,
        params,
        stack,
        ts: Date.now(),
      });
    });
    this.cdpLogger?.({ method, params, targetId: null });

Review comment (bug):
I think this might have a stealth/reliability impact, gaps in runtime performance metrics as a result of debuggerOnStart are one of the signals bot-blockers use iirc. Also there are weird situations with using `waitForDebuggerOnStart` on service workers.

might be worth only setting this if there are initScripts needed

Further reading on issues with `waitForDebuggerOnStart`:

- https://github.com/puppeteer/puppeteer/issues/3667#issuecomment-1588632073
- https://issues.chromium.org/issues/40280584
- https://issues.chromium.org/issues/428100629
- https://issues.chromium.org/issues/40269916
- https://issues.chromium.org/issues/376417348 reports one such weirdness:
	> This works fine (as expected) while a new target is opened with a normal click (left click). The new tab is stuck in hanging/loading state until runIfWaitingForDebugger command is sent. We receive waitingForDebugger: true for attachedTotTarget event.
	
	> However, when a tab is opened using ctrl + click (command + click on MAC), browser behaves differently. The tab starts loading at once, without waiting for runIfWaitingForDebugger command. In attachedTotTarget waitingForDebugger parameter is still true.

Code after:
      // Reason is a Buffer in ws; stringify defensively
      const why = `socket-close code=${code} reason=${String(reason || "")}`;
      this.emitTransportClosed(why);
    });

    this.ws.on("error", (err) => {
      const why = `socket-error ${err?.message ?? String(err)}`;
      this.emitTransportClosed(why);
    });
    this.ws.on("message", (data) => this.onMessage(data.toString()));
  }

  static async connect(wsUrl: string): Promise<CdpConnection> {
    const ws = new WebSocket(wsUrl);
    await new Promise<void>((resolve, reject) => {
      ws.once("open", () => resolve());
      ws.once("error", (e) => reject(e));
    });
    return new CdpConnection(ws);
  }

  async enableAutoAttach(): Promise<void> {
    await this.send("Target.setAutoAttach", {
      autoAttach: true,
      flatten: true,
      waitForDebuggerOnStart: true,
    });
    await this.send("Target.setDiscoverTargets", { discover: true });
  }

  async send<R = unknown>(method: string, params?: object): Promise<R> {
    const id = this.nextId++;
    const payload = { id, method, params };
    const stack = new Error().stack?.split("\n").slice(1, 4).join("\n");
    const p = new Promise<R>((resolve, reject) => {
      this.inflight.set(id, {
        resolve,
        reject,
        sessionId: null,
        method,
        params,
        stack,
        ts: Date.now(),
      });
    });
    this.cdpLogger?.({ method, params, targetId: null });
    this.ws.send(JSON.stringify(payload));
    return p;
  }

  on<P = unknown>(event: string, handler: (params: P) => void): void {