diff --git a/.changeset/add-browserbase-url-accessors.md b/.changeset/add-browserbase-url-accessors.md new file mode 100644 index 000000000..dedb40ca4 --- /dev/null +++ b/.changeset/add-browserbase-url-accessors.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +Add Browserbase session URL and debug URL accessors diff --git a/packages/core/lib/v3/v3.ts b/packages/core/lib/v3/v3.ts index a5c030fa9..a66c2a26f 100644 --- a/packages/core/lib/v3/v3.ts +++ b/packages/core/lib/v3/v3.ts @@ -137,9 +137,17 @@ export class V3 { private readonly domSettleTimeoutMs?: number; private _isClosing = false; public browserbaseSessionId?: string; + private browserbaseSessionUrl?: string; + private browserbaseDebugUrl?: string; public get browserbaseSessionID(): string | undefined { return this.browserbaseSessionId; } + public get browserbaseSessionURL(): string | undefined { + return this.browserbaseSessionUrl; + } + public get browserbaseDebugURL(): string | undefined { + return this.browserbaseDebugUrl; + } private _onCdpClosed = (why: string) => { // Single place to react to the transport closing this._immediateShutdown(`CDP transport closed: ${why}`).catch(() => {}); @@ -679,6 +687,7 @@ export class V3 { } as unknown as import("chrome-launcher").LaunchedChrome, ws: lbo.cdpUrl, }; + this.resetBrowserbaseSessionMetadata(); // Post-connect settings (downloads and viewport) if provided await this._applyPostConnectLocalOptions(lbo); return; @@ -767,7 +776,7 @@ export class V3 { createdTempProfile: createdTemp, preserveUserDataDir: !!lbo.preserveUserDataDir, }; - this.browserbaseSessionId = undefined; + this.resetBrowserbaseSessionMetadata(); // Post-connect settings (downloads and viewport) if provided await this._applyPostConnectLocalOptions(lbo); @@ -841,18 +850,21 @@ export class V3 { await this._ensureBrowserbaseDownloadsEnabled(); + const resumed = !!this.opts.browserbaseSessionID; + let debugUrl: string | undefined; + try { + const dbg = (await bb.sessions.debug(sessionId)) as unknown as { + debuggerUrl?: string; + }; + debugUrl = dbg?.debuggerUrl; + } catch { + // Ignore debug fetch failures; continue with sessionUrl only + } + const sessionUrl = `https://www.browserbase.com/sessions/${sessionId}`; + this.browserbaseSessionUrl = sessionUrl; + this.browserbaseDebugUrl = debugUrl; + try { - const resumed = !!this.opts.browserbaseSessionID; - let debugUrl: string | undefined; - try { - const dbg = (await bb.sessions.debug(sessionId)) as unknown as { - debuggerUrl?: string; - }; - debugUrl = dbg?.debuggerUrl; - } catch { - // Ignore debug fetch failures; continue with sessionUrl only - } - const sessionUrl = `https://www.browserbase.com/sessions/${sessionId}`; this.logger({ category: "init", message: resumed @@ -926,6 +938,12 @@ export class V3 { } } + private resetBrowserbaseSessionMetadata(): void { + this.browserbaseSessionId = undefined; + this.browserbaseSessionUrl = undefined; + this.browserbaseDebugUrl = undefined; + } + /** * Run an "act" instruction through the ActHandler. * @@ -1277,7 +1295,7 @@ export class V3 { this.state = { kind: "UNINITIALIZED" }; this.ctx = null; this._isClosing = false; - this.browserbaseSessionId = undefined; + this.resetBrowserbaseSessionMetadata(); try { unbindInstanceLogger(this.instanceId); } catch { diff --git a/packages/core/tests/browserbase-session-accessors.test.ts b/packages/core/tests/browserbase-session-accessors.test.ts new file mode 100644 index 000000000..1621596df --- /dev/null +++ b/packages/core/tests/browserbase-session-accessors.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { V3 } from "../lib/v3/v3"; + +const MOCK_SESSION_ID = "session-123"; +const MOCK_SESSION_URL = `https://www.browserbase.com/sessions/${MOCK_SESSION_ID}`; +const MOCK_DEBUG_URL = `https://debug.browserbase.com/${MOCK_SESSION_ID}`; + +vi.mock("../lib/v3/understudy/context", () => { + class MockConnection { + onTransportClosed = vi.fn(); + offTransportClosed = vi.fn(); + send = vi.fn(async () => {}); + } + + class MockV3Context { + static async create(): Promise { + return new MockV3Context(); + } + + conn = new MockConnection(); + + pages(): never[] { + return []; + } + + async close(): Promise { + // noop + } + } + + return { V3Context: MockV3Context }; +}); + +vi.mock("../lib/v3/launch/browserbase", () => ({ + createBrowserbaseSession: vi.fn(async () => ({ + ws: "wss://mock-browserbase", + sessionId: MOCK_SESSION_ID, + bb: { + sessions: { + debug: vi.fn(async () => ({ debuggerUrl: MOCK_DEBUG_URL })), + }, + }, + })), +})); + +vi.mock("../lib/v3/launch/local", () => ({ + launchLocalChrome: vi.fn(async () => ({ + ws: "ws://local-cdp", + chrome: { kill: vi.fn(async () => {}) }, + })), +})); + +describe("browserbase accessors", () => { + beforeEach(() => { + process.env.BROWSERBASE_API_KEY = "fake-key"; + process.env.BROWSERBASE_PROJECT_ID = "fake-project"; + }); + + afterEach(() => { + delete process.env.BROWSERBASE_API_KEY; + delete process.env.BROWSERBASE_PROJECT_ID; + vi.clearAllMocks(); + }); + + it("exposes Browserbase session and debug URLs after init", async () => { + const v3 = new V3({ + env: "BROWSERBASE", + disableAPI: true, + verbose: 0, + }); + + try { + await v3.init(); + + expect(v3.browserbaseSessionURL).toBe(MOCK_SESSION_URL); + expect(v3.browserbaseDebugURL).toBe(MOCK_DEBUG_URL); + } finally { + await v3.close().catch(() => {}); + } + }); + + it("clears stored URLs after close", async () => { + const v3 = new V3({ + env: "BROWSERBASE", + disableAPI: true, + verbose: 0, + }); + + await v3.init(); + await v3.close(); + + expect(v3.browserbaseSessionURL).toBeUndefined(); + expect(v3.browserbaseDebugURL).toBeUndefined(); + }); +}); + +describe("local accessors", () => { + it("stay empty for LOCAL environments", async () => { + const v3 = new V3({ + env: "LOCAL", + disableAPI: true, + verbose: 0, + localBrowserLaunchOptions: { + cdpUrl: "ws://local-existing-session", + }, + }); + + try { + await v3.init(); + expect(v3.browserbaseSessionURL).toBeUndefined(); + expect(v3.browserbaseDebugURL).toBeUndefined(); + } finally { + await v3.close().catch(() => {}); + } + }); +}); diff --git a/packages/docs/v3/references/stagehand.mdx b/packages/docs/v3/references/stagehand.mdx index 74a808cb9..31d600ffe 100644 --- a/packages/docs/v3/references/stagehand.mdx +++ b/packages/docs/v3/references/stagehand.mdx @@ -379,6 +379,36 @@ interface HistoryEntry { } ``` +### browserbaseSessionID + +Browserbase session identifier for the active Browserbase run. + +```typescript +const sessionId = stagehand.browserbaseSessionID; +``` + +**Type:** `string | undefined` — undefined for LOCAL runs or before `init()`. + +### browserbaseSessionURL + +Shareable link to the active Browserbase session dashboard. + +```typescript +const sessionUrl = stagehand.browserbaseSessionURL; +``` + +**Type:** `string | undefined` — undefined until a Browserbase session is active. + +### browserbaseDebugURL + +Debugger URL returned by Browserbase for direct CDP inspection. + +```typescript +const debugUrl = stagehand.browserbaseDebugURL; +``` + +**Type:** `string | undefined` — undefined for LOCAL runs or if Browserbase doesn’t provide one. + ## Code Examples