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

Code before:
      } catch {
        //
      }
    }
  }

  private constructor(ws: WebSocket) {
    this.ws = ws;
    this.ws.on("close", (code, reason) => {
      // 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, {
      headers: {
        "User-Agent": `Stagehand/${STAGEHAND_VERSION}`,
      },
    });
    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 };

Review comment (suggestion):
```suggestion
        "User-Agent": `Stagehand/${STAGEHAND_VERSION} IS_SEA=${IS_SEA} BB_ENV=${BB_ENV}`,
```

We should add these env vars too eventually (I can do it in a separate pr because `IS_SEA=1` would have to be set in all the SEA build first).

Code after:
      } catch {
        //
      }
    }
  }

  private constructor(ws: WebSocket) {
    this.ws = ws;
    this.ws.on("close", (code, reason) => {
      // 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> {
    // Include User-Agent header for server-side observability and version tracking
    const ws = new WebSocket(wsUrl, {
      headers: {
        "User-Agent": `Stagehand/${STAGEHAND_VERSION}`,
      },
    });
    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++;