Skip to content
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ jobs:
github.repository == 'stainless-sdks/stagehand-go' &&
(github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Get GitHub OIDC Token
if: |-
github.repository == 'stainless-sdks/stagehand-go' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());

Expand All @@ -53,10 +53,10 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: ./go.mod

Expand All @@ -68,10 +68,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/stagehand-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: ./go.mod

Expand Down
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.20.0"
".": "3.21.0"
}
4 changes: 2 additions & 2 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/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml
openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-80502d74c1be605e77d45ff2b54297fe34ce85dbad1e8f2dfa30ba6d09601219.yml
openapi_spec_hash: fd62f768756a400c3ecd695bfcf3845a
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.21.0 (2026-05-20)

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

### Features

* [feat]: add `ignoreSelectors` to `observe()` ([501d30b](https://github.com/browserbase/stagehand-go/commit/501d30b5aa52a72eed5a25b064609b5c7c84d733))
* Add `screenshot` option to Extract ([ba29e1e](https://github.com/browserbase/stagehand-go/commit/ba29e1e97d4744a504adf2990fa4cc1eeabf9bba))
* **client:** optimize json encoder for internal types ([539a65e](https://github.com/browserbase/stagehand-go/commit/539a65e84dfdd9a1330c6142d229ea47b2575376))
* STG-1756 add Vertex auth params to Stagehand spec ([b381980](https://github.com/browserbase/stagehand-go/commit/b381980c635b1d7968a2022709fa91921e77556e))


### Bug Fixes

* **go:** avoid panic when http.DefaultTransport is wrapped ([cea79d5](https://github.com/browserbase/stagehand-go/commit/cea79d5e25d6f072af7b2dde01d34be71106cb4b))


### Chores

* redact api-key headers in debug logs ([6378688](https://github.com/browserbase/stagehand-go/commit/637868870522a96e2e1d585949926a6116cb1e01))

## 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)
Expand Down
2 changes: 1 addition & 1 deletion 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.20.0'
go get -u 'github.com/browserbase/stagehand-go@v3.21.0'
```

<!-- x-release-please-end -->
Expand Down
18 changes: 12 additions & 6 deletions default_http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import (
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.
// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the
// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout]
// is set so stuck connections fail fast instead of compounding across retries.
// If [http.DefaultTransport] has been wrapped (for example by otelhttp for
// distributed tracing), the wrapping is preserved and the header timeout is
// skipped.
func defaultHTTPClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout
return &http.Client{Transport: transport}
if t, ok := http.DefaultTransport.(*http.Transport); ok {
t = t.Clone()
t.ResponseHeaderTimeout = defaultResponseHeaderTimeout
return &http.Client{Transport: t}
}
return &http.Client{Transport: http.DefaultTransport}
}
21 changes: 15 additions & 6 deletions internal/encoding/json/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,21 @@ import (
// JSON cannot represent cyclic data structures and Marshal does not
// handle them. Passing cyclic structures to Marshal will result in
// an error.
func Marshal(v any) ([]byte, error) {
// EDIT(begin): add optimization options
func Marshal(v any, opts ...Option) ([]byte, error) {
// EDIT(end): add optimization options
e := newEncodeState()
defer encodeStatePool.Put(e)

// SHIM(begin): don't escape HTML by default
err := e.marshal(v, encOpts{escapeHTML: shims.EscapeHTMLByDefault})
// EDIT(begin): don't escape HTML by default, and apply options
encOpts := encOpts{escapeHTML: shims.EscapeHTMLByDefault}
if opts != nil {
encOpts = encOpts.apply(opts...)
}
err := e.marshal(v, encOpts)
// ORIGINAL:
// err := e.marshal(v, encOpts{escapeHTML: true})
// SHIM(end)
// EDIT(end)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -352,6 +358,9 @@ type encOpts struct {
// EDIT(begin): save the timefmt
timefmt string
// EDIT(end)
// EDIT(begin): add optimization to skip compaction
skipCompaction bool
// EDIT(end)
}

type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
Expand Down Expand Up @@ -483,7 +492,7 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
if err == nil {
e.Grow(len(b))
out := e.AvailableBuffer()
out, err = appendCompact(out, b, opts.escapeHTML)
out, err = appendCompact(out, b, opts)
e.Buffer.Write(out)
}
if err != nil {
Expand All @@ -509,7 +518,7 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
if err == nil {
e.Grow(len(b))
out := e.AvailableBuffer()
out, err = appendCompact(out, b, opts.escapeHTML)
out, err = appendCompact(out, b, opts)
e.Buffer.Write(out)
}
if err != nil {
Expand Down
17 changes: 14 additions & 3 deletions internal/encoding/json/indent.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

package json

import "bytes"
import (
"bytes"
)

// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029
Expand Down Expand Up @@ -41,12 +43,21 @@ func appendHTMLEscape(dst, src []byte) []byte {
func Compact(dst *bytes.Buffer, src []byte) error {
dst.Grow(len(src))
b := dst.AvailableBuffer()
b, err := appendCompact(b, src, false)
b, err := appendCompact(b, src, encOpts{})
dst.Write(b)
return err
}

func appendCompact(dst, src []byte, escape bool) ([]byte, error) {
func appendCompact(dst, src []byte, opts encOpts) ([]byte, error) {
// EDIT(begin): optimize for skipCompaction
if opts.skipCompaction {
dst = append(dst, src...)
return dst, nil
}

escape := opts.escapeHTML
// EDIT(end)

origLen := len(dst)
scan := newScanner()
defer freeScanner(scan)
Expand Down
24 changes: 24 additions & 0 deletions internal/encoding/json/opt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// EDIT(begin): add custom options for JSON encoding
package json

type Option func(*encOpts)

// Every time a sub-type of [json.Marshaler] is encountered,
// skip a redundant and costly compaction step, trust it to self-compact.
//
// This is a divergence from the standard library behavior, and is only guaranteed
// safe with SDK types.
func WithSkipCompaction(b bool) Option {
return func(eos *encOpts) {
eos.skipCompaction = true
}
}

func (eos encOpts) apply(opts ...Option) encOpts {
for _, opt := range opts {
opt(&eos)
}
return eos
}

// EDIT(end)
53 changes: 28 additions & 25 deletions internal/encoding/json/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package json

import (
"bytes"
"errors"
"io"
)

Expand Down Expand Up @@ -253,30 +252,34 @@ func (enc *Encoder) SetEscapeHTML(on bool) {
enc.escapeHTML = on
}

// RawMessage is a raw encoded JSON value.
// It implements [Marshaler] and [Unmarshaler] and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte

// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}

// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}

var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)
// EDIT(begin): remove RawMessage
//
// // RawMessage is a raw encoded JSON value.
// // It implements [Marshaler] and [Unmarshaler] and can
// // be used to delay JSON decoding or precompute a JSON encoding.
// type RawMessage []byte
//
// // MarshalJSON returns m as the JSON encoding of m.
// func (m RawMessage) MarshalJSON() ([]byte, error) {
// if m == nil {
// return []byte("null"), nil
// }
// return m, nil
// }
//
// // UnmarshalJSON sets *m to a copy of data.
// func (m *RawMessage) UnmarshalJSON(data []byte) error {
// if m == nil {
// return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
// }
// *m = append((*m)[0:0], data...)
// return nil
// }
//
// var _ Marshaler = (*RawMessage)(nil)
// var _ Unmarshaler = (*RawMessage)(nil)
//
// EDIT(end)

// A Token holds a value of one of these types:
//
Expand Down
2 changes: 1 addition & 1 deletion internal/encoding/json/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func timeMarshalEncoder(e *encodeState, v reflect.Value, opts encOpts) bool {
if b != nil {
e.Grow(len(b))
out := e.AvailableBuffer()
out, _ = appendCompact(out, b, opts.escapeHTML)
out, _ = appendCompact(out, b, opts)
e.Buffer.Write(out)
return true
}
Expand Down
2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

package internal

const PackageVersion = "3.20.0" // x-release-please-version
const PackageVersion = "3.21.0" // x-release-please-version
46 changes: 44 additions & 2 deletions option/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"net/http/httputil"
)

// sensitiveLogHeaders are redacted before request and response content is
// written to the debug logger.
var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie", "x-bb-api-key", "x-bb-project-id", "x-model-api-key"}

// WithDebugLog logs the HTTP request and response content.
// If the logger parameter is nil, it uses the default logger.
//
Expand All @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption {
logger = log.Default()
}

if reqBytes, err := httputil.DumpRequest(req, true); err == nil {
if reqBytes, err := dumpRedactedRequest(req); err == nil {
logger.Printf("Request Content:\n%s\n", reqBytes)
}

Expand All @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption {
return resp, err
}

if respBytes, err := httputil.DumpResponse(resp, true); err == nil {
if respBytes, err := dumpRedactedResponse(resp); err == nil {
logger.Printf("Response Content:\n%s\n", respBytes)
}

return resp, err
})
}

// dumpRedactedRequest dumps req with sensitive headers replaced. The
// original headers are restored via defer so a panic in DumpRequest cannot
// leak the placeholder map into the live request sent downstream.
func dumpRedactedRequest(req *http.Request) ([]byte, error) {
origHeaders := req.Header
req.Header = redactDebugHeaders(origHeaders)
defer func() { req.Header = origHeaders }()
return httputil.DumpRequest(req, true)
}

func dumpRedactedResponse(resp *http.Response) ([]byte, error) {
origHeaders := resp.Header
resp.Header = redactDebugHeaders(origHeaders)
defer func() { resp.Header = origHeaders }()
return httputil.DumpResponse(resp, true)
}

func redactDebugHeaders(headers http.Header) http.Header {
var redacted http.Header
for _, name := range sensitiveLogHeaders {
values := headers.Values(name)
if len(values) == 0 {
continue
}
if redacted == nil {
redacted = headers.Clone()
}
redacted.Del(name)
for range values {
redacted.Add(name, "***")
}
}
if redacted == nil {
return headers
}
return redacted
}
4 changes: 2 additions & 2 deletions packages/param/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func MarshalWithExtras[T ParamStruct, R any](f T, underlying any, extras map[str
} else if ovr, ok := f.Overrides(); ok {
return shimjson.Marshal(ovr)
} else {
return shimjson.Marshal(underlying)
return shimjson.Marshal(underlying, shimjson.WithSkipCompaction(true))
}
}

Expand Down Expand Up @@ -96,7 +96,7 @@ func MarshalUnion[T ParamStruct](metadata T, variants ...any) ([]byte, error) {
Err: fmt.Errorf("expected union to have only one present variant, got %d", nPresent),
}
}
return shimjson.Marshal(variants[presentIdx])
return shimjson.Marshal(variants[presentIdx], shimjson.WithSkipCompaction(true))
}

// typeFor is shimmed from Go 1.23 "reflect" package
Expand Down
Loading
Loading