Skip to content

Commit 100e201

Browse files
authored
feat(sdk-go): add Browserbase ClientOptions to launch and connect (#3123)
# why Stacked on #3119 (Python), which is stacked on #3118 (TypeScript). Those PRs add Browserbase client options to `browserbase.launch()` / `browserbase.connect()` in TS and Python. This PR adds the Go equivalent, so the three SDKs are at parity again and the parity test no longer needs a Go exemption. # what changed - New `BrowserbaseClientOptions` struct, set as `ClientOptions *BrowserbaseClientOptions` on both `BrowserbaseLaunchOptions` and `BrowserbaseConnectOptions`: - `Timeout time.Duration`: applies to each attempt; 0 keeps the 60s default. - `MaxRetries *int`: nil keeps the default of 2. - `DefaultHeaders map[string]string`, `DefaultQuery map[string]string`. - `HTTPClient *http.Client`: owned by the caller and never modified. - Go talks to Browserbase over its own `net/http` client, so the internal options it already had (HTTP client, max retries) are now exposed, plus headers and query. - The timeout is applied per attempt through the request context, both for the default client and for a client the caller passes in, and it also covers reading the response body. The default client no longer sets `http.Client.Timeout` itself. If an attempt times out, only requests that are safe to resend (retrieve, release, delete) are retried, each with a new deadline. Creates fail immediately. - Negative `Timeout` or `MaxRetries`, invalid header names, and header values containing control characters are rejected before any request is sent. - `ClientOptions` is never sent in the session-create body; a test checks the body contents. - Removes `pendingGoBrowserFields` from the cross-language parity test. - Docs: the client-options section is a TypeScript / Python / Go tab group again. - Adds a minor changeset for `@browserbasehq/stagehand-go`. This PR is the only change in the stack that releases Go. # header precedence Same rule as TS and Python: `DefaultHeaders` override Stagehand's own headers on conflict, including `X-BB-API-Key` and `User-Agent`. Both Browserbase SDKs apply caller default headers last; this was checked in their source (`@browserbasehq/sdk` `index.js` `defaultHeaders`, and the Python `_base_client`). Go still rejects invalid header names or values before sending. # test plan - [x] `go vet`; gofmt (prints nothing); `go test -race` on every package except examples; generator `--check`; `scripts/check-examples.sh` - [x] New `browserbase_client_options_test.go` uses `httptest` servers to cover: - options reaching requests, and later changes to the caller's maps having no effect; - default headers and query on POST and GET, with caller defaults overriding Stagehand's headers (mutation-checked); - `MaxRetries` set to 0 and 1, and the default; - the per-attempt timeout, including a retry getting a new deadline and the deadline covering the response body; - a caller `HTTPClient` being used and left unmodified; - invalid options failing with zero requests reaching the server, on both launch and connect; - full launch and connect through the real factory. - [x] Mutation-checked: with the per-attempt deadline removed, the timeout test fails in about 5s with a clear message. - [x] `rules/ast-grep` parity tests (Go exemption removed), docs tests, the TS suite, oxfmt, oxlint, `check-changesets` - [ ] Plain `go test ./...` fails only because `packages/sdk-go/examples` doesn't build as a single package; that was already the case. CI and the justfile exclude `/examples`. - [ ] No live Browserbase session was launched
1 parent a837acf commit 100e201

7 files changed

Lines changed: 782 additions & 67 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand-go": minor
3+
---
4+
5+
Add `ClientOptions` to `LaunchBrowserbase` and `ConnectBrowserbase`.

‎packages/docs/v4/configuration/browser.mdx‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,10 @@ The Browserbase URL controls extension, session, and connection requests that `b
180180

181181
### Browserbase client options
182182

183-
`browserbase.launch()` and `browserbase.connect()` create a Browserbase SDK client for session and extension requests. Pass `clientOptions` (TypeScript) or `client_options` (Python) to tune that client's timeout, retries, default headers, and default query parameters. TypeScript also accepts a custom `fetch`, and Python accepts an `httpx.AsyncClient` as `http_client`. `apiKey` and `baseUrl` stay top-level, so one key authenticates both Browserbase and Stagehand.
183+
`browserbase.launch()` and `browserbase.connect()` create a Browserbase client for session and extension requests. Pass `clientOptions` (TypeScript), `client_options` (Python), or `ClientOptions` (Go) to tune that client's timeout, retries, default headers, and default query parameters. TypeScript also accepts a custom `fetch`, Python accepts an `httpx.AsyncClient` as `http_client`, and Go accepts an `*http.Client` as `HTTPClient`. `apiKey` and `baseUrl` stay top-level, so one key authenticates both Browserbase and Stagehand.
184184

185+
<Tabs>
186+
<Tab title="TypeScript">
185187
```typescript
186188
const browser = await browserbase.launch({
187189
apiKey: process.env.BROWSERBASE_API_KEY,
@@ -192,7 +194,9 @@ const browser = await browserbase.launch({
192194
},
193195
});
194196
```
197+
</Tab>
195198

199+
<Tab title="Python">
196200
```python
197201
browser = await browserbase.launch(
198202
api_key=os.environ["BROWSERBASE_API_KEY"],
@@ -203,8 +207,28 @@ browser = await browserbase.launch(
203207
},
204208
)
205209
```
210+
</Tab>
211+
212+
<Tab title="Go">
213+
```go
214+
maxRetries := 1
215+
browser, err := stagehand.LaunchBrowserbase(ctx, stagehand.BrowserbaseLaunchOptions{
216+
APIKey: os.Getenv("BROWSERBASE_API_KEY"),
217+
ClientOptions: &stagehand.BrowserbaseClientOptions{
218+
Timeout: 30 * time.Second, // per attempt
219+
MaxRetries: &maxRetries,
220+
DefaultHeaders: map[string]string{"X-Request-Source": "my-app"},
221+
// HTTPClient: myHTTPClient, // optional caller-owned *http.Client
222+
},
223+
})
224+
if err != nil {
225+
return err
226+
}
227+
```
228+
</Tab>
229+
</Tabs>
206230

207-
Units follow each Browserbase SDK: TypeScript timeouts are in milliseconds and Python timeouts are in seconds. Stagehand never closes an `http_client` you pass; keep it open until `browser.close()` returns, because releasing the session uses it.
231+
Units follow each SDK: TypeScript timeouts are in milliseconds, Python timeouts are in seconds, and Go uses a `time.Duration` that bounds each attempt. Stagehand never closes an `http_client` you pass; keep it open until `browser.close()` returns, because releasing the session uses it. Go never modifies an `HTTPClient` you pass, so its own `Timeout` and `Transport` still apply. In every SDK, default headers override the client's own headers on conflict, including `X-BB-API-Key` and `User-Agent`. Go rejects invalid header names or values before sending any request.
208232

209233
### Environment variables
210234

‎packages/sdk-go/browser_factories.go‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"net/http"
89
"strings"
10+
"time"
911

1012
"github.com/browserbase/stagehand/packages/sdk-go/v4/internal/extensionassets"
1113
)
@@ -40,10 +42,28 @@ type LocalBrowserConnectOptions struct {
4042
ExtensionID string
4143
}
4244

45+
// BrowserbaseClientOptions configures the HTTP client Stagehand uses for
46+
// Browserbase session and extension requests.
47+
type BrowserbaseClientOptions struct {
48+
// Timeout bounds each request attempt. Zero keeps the 60 second default.
49+
Timeout time.Duration
50+
// MaxRetries caps retries of replay-safe requests. Nil keeps the default of 2.
51+
MaxRetries *int
52+
// DefaultHeaders are added to every request and override Stagehand's own
53+
// headers (including X-BB-API-Key and User-Agent) on conflict.
54+
DefaultHeaders map[string]string
55+
// DefaultQuery parameters are added to every request URL.
56+
DefaultQuery map[string]string
57+
// HTTPClient sends the requests. Stagehand never modifies it; Timeout is
58+
// applied per attempt through the request context. Nil uses a default client.
59+
HTTPClient *http.Client
60+
}
61+
4362
// BrowserbaseLaunchOptions configures a newly launched Browserbase session.
4463
type BrowserbaseLaunchOptions struct {
4564
APIKey string
4665
BaseURL string
66+
ClientOptions *BrowserbaseClientOptions
4767
BrowserSettings *BrowserbaseBrowserSettings
4868
ExtensionID *string
4969
KeepAlive *bool
@@ -55,10 +75,11 @@ type BrowserbaseLaunchOptions struct {
5575

5676
// BrowserbaseConnectOptions configures a connection to an existing Browserbase session.
5777
type BrowserbaseConnectOptions struct {
58-
APIKey string
59-
BaseURL string
60-
SessionID string
61-
ExtensionID string
78+
APIKey string
79+
BaseURL string
80+
ClientOptions *BrowserbaseClientOptions
81+
SessionID string
82+
ExtensionID string
6283
}
6384

6485
type browserbaseFactoryClient interface {
@@ -68,7 +89,7 @@ type browserbaseFactoryClient interface {
6889

6990
type browserFactoryDependencies struct {
7091
launchLocal func(context.Context, LocalBrowserLaunchOptions) (resolvedBrowserSource, error)
71-
createBrowserbaseClient func(string, string) (browserbaseFactoryClient, error)
92+
createBrowserbaseClient func(apiKey string, baseURL string, clientOptions *BrowserbaseClientOptions) (browserbaseFactoryClient, error)
7293
connectCDP func(context.Context, cdpClientOptions) (*cdpClient, error)
7394
materializeExtension func() (string, func() error, error)
7495
commandSender func(*cdpClient) browserCommandSender
@@ -198,7 +219,7 @@ func launchBrowserbaseWithDependencies(ctx context.Context, options BrowserbaseL
198219
}
199220
defer cancelLifecycle()
200221

201-
client, err := browserbaseClientForFactory(options.APIKey, options.BaseURL, dependencies)
222+
client, err := browserbaseClientForFactory(options.APIKey, options.BaseURL, options.ClientOptions, dependencies)
202223
if err != nil {
203224
return nil, err
204225
}
@@ -232,7 +253,7 @@ func connectBrowserbaseWithDependencies(ctx context.Context, options Browserbase
232253
}
233254
defer cancelLifecycle()
234255

235-
client, err := browserbaseClientForFactory(options.APIKey, options.BaseURL, dependencies)
256+
client, err := browserbaseClientForFactory(options.APIKey, options.BaseURL, options.ClientOptions, dependencies)
236257
if err != nil {
237258
return nil, err
238259
}
@@ -249,19 +270,19 @@ func connectBrowserbaseWithDependencies(ctx context.Context, options Browserbase
249270
}, dependencies)
250271
}
251272

252-
func browserbaseClientForFactory(apiKey string, baseURL string, dependencies browserFactoryDependencies) (browserbaseFactoryClient, error) {
273+
func browserbaseClientForFactory(apiKey string, baseURL string, clientOptions *BrowserbaseClientOptions, dependencies browserFactoryDependencies) (browserbaseFactoryClient, error) {
253274
if strings.TrimSpace(apiKey) == "" {
254275
return nil, errors.New("stagehand Browserbase API key is required")
255276
}
256277
factory := dependencies.createBrowserbaseClient
257278
if factory == nil {
258-
factory = func(apiKey string, baseURL string) (browserbaseFactoryClient, error) {
279+
factory = func(apiKey string, baseURL string, clientOptions *BrowserbaseClientOptions) (browserbaseFactoryClient, error) {
259280
return newBrowserbaseSessionClient(apiKey, browserbaseSessionClientOptions{
260-
http: browserbaseHTTPClientOptions{baseURL: baseURL},
281+
http: browserbaseHTTPClientOptionsFor(baseURL, clientOptions),
261282
})
262283
}
263284
}
264-
client, err := factory(apiKey, baseURL)
285+
client, err := factory(apiKey, baseURL, clientOptions)
265286
if err != nil {
266287
return nil, fmt.Errorf("create Stagehand Browserbase client: %w", err)
267288
}

‎packages/sdk-go/browser_test.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,12 @@ func TestBrowserbaseFactoryMetadataAndExtensionRouting(t *testing.T) {
646646
}
647647
var connected cdpClientOptions
648648
var configuredBaseURL string
649+
var configuredClientOptions *BrowserbaseClientOptions
650+
clientOptions := &BrowserbaseClientOptions{Timeout: time.Second, MaxRetries: testPointer(0)}
649651
dependencies := browserFactoryDependencies{
650-
createBrowserbaseClient: func(_ string, baseURL string) (browserbaseFactoryClient, error) {
652+
createBrowserbaseClient: func(_ string, baseURL string, clientOptions *BrowserbaseClientOptions) (browserbaseFactoryClient, error) {
651653
configuredBaseURL = baseURL
654+
configuredClientOptions = clientOptions
652655
return client, nil
653656
},
654657
connectCDP: func(_ context.Context, options cdpClientOptions) (*cdpClient, error) {
@@ -659,10 +662,10 @@ func TestBrowserbaseFactoryMetadataAndExtensionRouting(t *testing.T) {
659662
var browser *Browser
660663
var err error
661664
if test.connect {
662-
browser, err = connectBrowserbaseWithDependencies(context.Background(), BrowserbaseConnectOptions{APIKey: "key", BaseURL: "https://api.dev.browserbase.com", SessionID: "retrieved", ExtensionID: test.extensionID}, dependencies)
665+
browser, err = connectBrowserbaseWithDependencies(context.Background(), BrowserbaseConnectOptions{APIKey: "key", BaseURL: "https://api.dev.browserbase.com", ClientOptions: clientOptions, SessionID: "retrieved", ExtensionID: test.extensionID}, dependencies)
663666
} else {
664667
browser, err = launchBrowserbaseWithDependencies(context.Background(), BrowserbaseLaunchOptions{
665-
APIKey: "key", BaseURL: "https://api.dev.browserbase.com", ExtensionID: &extensionID, KeepAlive: &keepAlive,
668+
APIKey: "key", BaseURL: "https://api.dev.browserbase.com", ClientOptions: clientOptions, ExtensionID: &extensionID, KeepAlive: &keepAlive,
666669
Region: &region, UserMetadata: userMetadata,
667670
}, dependencies)
668671
}
@@ -672,6 +675,9 @@ func TestBrowserbaseFactoryMetadataAndExtensionRouting(t *testing.T) {
672675
if configuredBaseURL != "https://api.dev.browserbase.com" {
673676
t.Fatalf("Browserbase base URL = %q", configuredBaseURL)
674677
}
678+
if configuredClientOptions != clientOptions {
679+
t.Fatalf("Browserbase client options = %#v, want %#v", configuredClientOptions, clientOptions)
680+
}
675681
if !test.connect {
676682
created := client.createOptions
677683
if created.ExtensionID == nil || *created.ExtensionID != extensionID ||

0 commit comments

Comments
 (0)