Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.19.3"
".": "3.20.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 8
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-b969ce378479c79ee64c05127c0ed6c6ce2edbee017ecd037242fb618a5ebc9f.yml
openapi_spec_hash: a24aabaa5214effb679808b7f2be0ad4
config_hash: a962ae71493deb11a1c903256fb25386
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml
openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5
config_hash: 1fb12ae9b478488bc1e56bfbdc210b01
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 3.20.0 (2026-05-06)

Full Changelog: [v3.19.3...v3.20.0](https://github.com/browserbase/stagehand-go/compare/v3.19.3...v3.20.0)

### Features

* [feat]: add `ignoreSelectors` to `extract()` ([5471190](https://github.com/browserbase/stagehand-go/commit/547119099d9201d53820f12345e44acd940cccc6))
* [STG-1798] feat: support Browserbase verified sessions ([30133d3](https://github.com/browserbase/stagehand-go/commit/30133d320e2093815d9714e32a64ffe6242b94aa))
* [STG-1808] Deprecate Browserbase project ID ([fa76f5f](https://github.com/browserbase/stagehand-go/commit/fa76f5f6c41e668a9d22a75d4a9ab92829a36f4e))
* Bedrock auth passthrough ([b41e3cb](https://github.com/browserbase/stagehand-go/commit/b41e3cb38fe07e0d19f8204b106320e3dee9c50b))
* **go:** add default http client with timeout ([a8bc1d5](https://github.com/browserbase/stagehand-go/commit/a8bc1d57f59c471301dce65cbbabffafeaa3ab6b))
* remove experimental requirement on agent variables ([#2079](https://github.com/browserbase/stagehand-go/issues/2079)) ([9316089](https://github.com/browserbase/stagehand-go/commit/93160890e958a17d3159273abc12a33c5b1f9d57))
* Revert "[STG-1573] Add providerOptions for extensible model auth ([#1822](https://github.com/browserbase/stagehand-go/issues/1822))" ([f56e93f](https://github.com/browserbase/stagehand-go/commit/f56e93ff49643c78a6a39da36b30a010dbf59b7d))
* support setting headers via env ([661576d](https://github.com/browserbase/stagehand-go/commit/661576dfbbaba6e019e9f32e05f6484ec3f17c5b))


### Chores

* avoid embedding reflect.Type for dead code elimination ([f071757](https://github.com/browserbase/stagehand-go/commit/f0717577f5de09f968fac0178dacd8662ec40c44))
* **internal:** more robust bootstrap script ([6fc0d10](https://github.com/browserbase/stagehand-go/commit/6fc0d10fc68841748a3f99cfbdc85930b18455d0))

## 3.19.3 (2026-04-03)

Full Changelog: [v3.18.0...v3.19.3](https://github.com/browserbase/stagehand-go/compare/v3.18.0...v3.19.3)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/browserbase/stagehand-go@v3.19.3'
go get -u 'github.com/browserbase/stagehand-go@v3.20.0'
```

<!-- x-release-please-end -->
Expand Down Expand Up @@ -192,10 +192,10 @@ func main() {

Set your environment variables (from `examples/.env.example`):

- `STAGEHAND_API_URL`
- `MODEL_API_KEY`
- `BROWSERBASE_API_KEY`
- `BROWSERBASE_PROJECT_ID`

`STAGEHAND_API_URL` is optional and defaults to the hosted Stagehand API. `STAGEHAND_BASE_URL` remains supported as a deprecated fallback when `STAGEHAND_API_URL` is unset.

```bash
cp examples/.env.example examples/.env
Expand Down
30 changes: 19 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"os"
"slices"
"strings"

"github.com/browserbase/stagehand-go/v3/internal/requestconfig"
"github.com/browserbase/stagehand-go/v3/option"
Expand All @@ -21,30 +22,37 @@ type Client struct {
}

// DefaultClientOptions read from the environment (BROWSERBASE_API_KEY,
// MODEL_API_KEY, BROWSERBASE_PROJECT_ID, STAGEHAND_BASE_URL). This should be used
// to initialize new clients.
// MODEL_API_KEY, STAGEHAND_API_URL, with STAGEHAND_BASE_URL as a fallback). This
// should be used to initialize new clients.
func DefaultClientOptions() []option.RequestOption {
defaults := []option.RequestOption{option.WithEnvironmentProduction()}
if o, ok := os.LookupEnv("STAGEHAND_BASE_URL"); ok {
defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentProduction()}
if o, ok := os.LookupEnv("STAGEHAND_API_URL"); ok {
defaults = append(defaults, option.WithBaseURL(o))
} else if o, ok := os.LookupEnv("STAGEHAND_BASE_URL"); ok {
defaults = append(defaults, option.WithBaseURL(o))
}
if o, ok := os.LookupEnv("BROWSERBASE_API_KEY"); ok {
defaults = append(defaults, option.WithBrowserbaseAPIKey(o))
}
if o, ok := os.LookupEnv("BROWSERBASE_PROJECT_ID"); ok {
defaults = append(defaults, option.WithBrowserbaseProjectID(o))
}
if o, ok := os.LookupEnv("MODEL_API_KEY"); ok {
defaults = append(defaults, option.WithModelAPIKey(o))
}
if o, ok := os.LookupEnv("STAGEHAND_CUSTOM_HEADERS"); ok {
for _, line := range strings.Split(o, "\n") {
colon := strings.Index(line, ":")
if colon >= 0 {
defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:])))
}
}
}
return defaults
}

// NewClient generates a new client with the default option read from the
// environment (BROWSERBASE_API_KEY, MODEL_API_KEY, BROWSERBASE_PROJECT_ID,
// STAGEHAND_BASE_URL). The option passed in as arguments are applied after these
// default arguments, and all option will be passed down to the services and
// requests that this client makes.
// environment (BROWSERBASE_API_KEY, MODEL_API_KEY, STAGEHAND_API_URL, with
// STAGEHAND_BASE_URL as a fallback). The option passed in as arguments are
// applied after these default arguments, and all option will be passed down to
// the services and requests that this client makes.
func NewClient(opts ...option.RequestOption) (r Client) {
opts = append(DefaultClientOptions(), opts...)
// BEGIN CUSTOM CODE - not generated by Stainless.
Expand Down
107 changes: 107 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"fmt"
"io"
"net/http"
"os"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -49,6 +51,111 @@ func TestUserAgentHeader(t *testing.T) {
}
}

func TestBaseURLFromStagehandAPIURLEnv(t *testing.T) {
t.Setenv("STAGEHAND_API_URL", "http://localhost:5000/from-api-env")
t.Setenv("STAGEHAND_BASE_URL", "http://localhost:5000/from-base-env")

var requestURL string
client := stagehand.NewClient(
option.WithBrowserbaseAPIKey("My Browserbase API Key"),
option.WithBrowserbaseProjectID("My Browserbase Project ID"),
option.WithModelAPIKey("My Model API Key"),
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
requestURL = req.URL.String()
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil
},
},
}),
)
_, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{
ModelName: "openai/gpt-5.4-mini",
})
if requestURL != "http://localhost:5000/from-api-env/v1/sessions/start" {
t.Errorf("Expected STAGEHAND_API_URL to take precedence, got: %s", requestURL)
}
}

func TestBaseURLFromLegacyStagehandBaseURLEnv(t *testing.T) {
oldAPIURL, hadAPIURL := os.LookupEnv("STAGEHAND_API_URL")
os.Unsetenv("STAGEHAND_API_URL")
t.Cleanup(func() {
if hadAPIURL {
os.Setenv("STAGEHAND_API_URL", oldAPIURL)
}
})
t.Setenv("STAGEHAND_BASE_URL", "http://localhost:5000/from-base-env")

var requestURL string
client := stagehand.NewClient(
option.WithBrowserbaseAPIKey("My Browserbase API Key"),
option.WithBrowserbaseProjectID("My Browserbase Project ID"),
option.WithModelAPIKey("My Model API Key"),
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
requestURL = req.URL.String()
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil
},
},
}),
)
_, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{
ModelName: "openai/gpt-5.4-mini",
})
if requestURL != "http://localhost:5000/from-base-env/v1/sessions/start" {
t.Errorf("Expected STAGEHAND_BASE_URL fallback, got: %s", requestURL)
}
}

func TestBrowserbaseProjectIDEnvIsIgnored(t *testing.T) {
t.Setenv("BROWSERBASE_PROJECT_ID", "My Browserbase Project ID")

var projectIDHeader string
client := stagehand.NewClient(
option.WithBrowserbaseAPIKey("My Browserbase API Key"),
option.WithModelAPIKey("My Model API Key"),
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
projectIDHeader = req.Header.Get("x-bb-project-id")
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil
},
},
}),
)
_, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{
ModelName: "openai/gpt-5.4-mini",
})
if projectIDHeader != "" {
t.Errorf("Expected x-bb-project-id header to be omitted, got: %s", projectIDHeader)
}
}

func TestBrowserbaseProjectIDOptionIsNoOp(t *testing.T) {
var projectIDHeader string
client := stagehand.NewClient(
option.WithBrowserbaseAPIKey("My Browserbase API Key"),
option.WithModelAPIKey("My Model API Key"),
option.WithBrowserbaseProjectID("My Browserbase Project ID"),
option.WithHTTPClient(&http.Client{
Transport: &closureTransport{
fn: func(req *http.Request) (*http.Response, error) {
projectIDHeader = req.Header.Get("x-bb-project-id")
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("{}"))}, nil
},
},
}),
)
_, _ = client.Sessions.Start(context.Background(), stagehand.SessionStartParams{
ModelName: "openai/gpt-5.4-mini",
})
if projectIDHeader != "" {
t.Errorf("Expected x-bb-project-id header to be omitted, got: %s", projectIDHeader)
}
}

func TestRetryAfter(t *testing.T) {
retryCountHeaders := make([]string, 0)
client := stagehand.NewClient(
Expand Down
24 changes: 24 additions & 0 deletions default_http_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

package stagehand

import (
"net/http"
"time"
)

// defaultResponseHeaderTimeout bounds the time between a fully written request
// and the server's response headers. It does not apply to the response body,
// so long-running streams are unaffected. Without this, a server that accepts
// the connection but never responds would hang the request indefinitely.
const defaultResponseHeaderTimeout = 10 * time.Minute

// defaultHTTPClient returns an [*http.Client] used when the caller does not
// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport]
// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections
// fail fast instead of compounding across retries.
func defaultHTTPClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout
return &http.Client{Transport: transport}
}
2 changes: 0 additions & 2 deletions examples/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
STAGEHAND_API_URL=https://api.stagehand.browserbase.com
MODEL_API_KEY=sk-proj-your-llm-api-key-here
BROWSERBASE_API_KEY=bb_live_your_api_key_here
BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here
6 changes: 0 additions & 6 deletions examples/basic/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
)

var requiredEnv = []string{
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}

func loadExampleEnv() {
Expand Down Expand Up @@ -54,10 +52,6 @@ func loadExampleEnv() {
if len(missing) > 0 {
panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)")
}

if os.Getenv("STAGEHAND_BASE_URL") == "" {
os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL"))
}
}

func findEnvPath() (string, bool) {
Expand Down
3 changes: 1 addition & 2 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//
// Prerequisites:
// - Set BROWSERBASE_API_KEY
// - Set BROWSERBASE_PROJECT_ID
// - Set MODEL_API_KEY
//
// Run:
Expand All @@ -24,7 +23,7 @@ import (

func main() {
loadExampleEnv()
client := stagehand.NewClient() // Uses env vars: BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY
client := stagehand.NewClient() // Uses env vars: BROWSERBASE_API_KEY and MODEL_API_KEY

startResponse, err := client.Sessions.Start(context.TODO(), stagehand.SessionStartParams{
ModelName: "anthropic/claude-sonnet-4-6",
Expand Down
6 changes: 0 additions & 6 deletions examples/chromedp_local_example/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
)

var requiredEnv = []string{
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}

func loadExampleEnv() {
Expand Down Expand Up @@ -54,10 +52,6 @@ func loadExampleEnv() {
if len(missing) > 0 {
panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)")
}

if os.Getenv("STAGEHAND_BASE_URL") == "" {
os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL"))
}
}

func findEnvPath() (string, bool) {
Expand Down
6 changes: 0 additions & 6 deletions examples/local/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
)

var requiredEnv = []string{
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}

func loadExampleEnv() {
Expand Down Expand Up @@ -54,10 +52,6 @@ func loadExampleEnv() {
if len(missing) > 0 {
panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)")
}

if os.Getenv("STAGEHAND_BASE_URL") == "" {
os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL"))
}
}

func findEnvPath() (string, bool) {
Expand Down
6 changes: 0 additions & 6 deletions examples/local_browser_chromedp_example/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
)

var requiredEnv = []string{
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}

func loadExampleEnv() {
Expand Down Expand Up @@ -54,10 +52,6 @@ func loadExampleEnv() {
if len(missing) > 0 {
panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)")
}

if os.Getenv("STAGEHAND_BASE_URL") == "" {
os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL"))
}
}

func findEnvPath() (string, bool) {
Expand Down
6 changes: 0 additions & 6 deletions examples/local_server_multiregion_browser_example/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import (
)

var requiredEnv = []string{
"STAGEHAND_API_URL",
"MODEL_API_KEY",
"BROWSERBASE_API_KEY",
"BROWSERBASE_PROJECT_ID",
}

func loadExampleEnv() {
Expand Down Expand Up @@ -54,10 +52,6 @@ func loadExampleEnv() {
if len(missing) > 0 {
panic("Missing required env vars: " + strings.Join(missing, ", ") + " (from examples/.env)")
}

if os.Getenv("STAGEHAND_BASE_URL") == "" {
os.Setenv("STAGEHAND_BASE_URL", os.Getenv("STAGEHAND_API_URL"))
}
}

func findEnvPath() (string, bool) {
Expand Down
Loading
Loading