diff --git a/packages/docs/docs.json b/packages/docs/docs.json
index a60d3d068..29e67d227 100644
--- a/packages/docs/docs.json
+++ b/packages/docs/docs.json
@@ -64,6 +64,17 @@
"v4/integrations/vercel-ai-sdk"
]
},
+ {
+ "group": "Observability providers",
+ "pages": [
+ "v4/observability/overview",
+ "v4/observability/braintrust",
+ "v4/observability/langfuse",
+ "v4/observability/langsmith",
+ "v4/observability/sentry",
+ "v4/observability/otel-collector"
+ ]
+ },
{
"group": "Best practices",
"pages": [
diff --git a/packages/docs/images/observability/braintrust.svg b/packages/docs/images/observability/braintrust.svg
new file mode 100644
index 000000000..8665a12bf
--- /dev/null
+++ b/packages/docs/images/observability/braintrust.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/packages/docs/images/observability/langfuse.svg b/packages/docs/images/observability/langfuse.svg
new file mode 100644
index 000000000..313290438
--- /dev/null
+++ b/packages/docs/images/observability/langfuse.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/packages/docs/images/observability/opentelemetry.svg b/packages/docs/images/observability/opentelemetry.svg
new file mode 100644
index 000000000..d7c352f83
--- /dev/null
+++ b/packages/docs/images/observability/opentelemetry.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/docs/images/observability/sentry.svg b/packages/docs/images/observability/sentry.svg
new file mode 100644
index 000000000..250601867
--- /dev/null
+++ b/packages/docs/images/observability/sentry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/docs/v4/configuration/observability.mdx b/packages/docs/v4/configuration/observability.mdx
index 3cd4b353e..36866f23c 100644
--- a/packages/docs/v4/configuration/observability.mdx
+++ b/packages/docs/v4/configuration/observability.mdx
@@ -1139,6 +1139,10 @@ On Browserbase, the session replay dashboard gives you the same timeline visuall
Stagehand emits OpenTelemetry spans for every operation and for every log record, and propagates W3C trace context across the SDK and runtime boundary. Point it at your own OTLP collector to see full traces alongside the rest of your system.
+
+ Choose a supported OTLP provider and copy its endpoint and authentication settings.
+
+
```typescript
diff --git a/packages/docs/v4/observability/braintrust.mdx b/packages/docs/v4/observability/braintrust.mdx
new file mode 100644
index 000000000..5393605c3
--- /dev/null
+++ b/packages/docs/v4/observability/braintrust.mdx
@@ -0,0 +1,127 @@
+---
+title: "Braintrust"
+sidebarTitle: "Braintrust"
+description: "Send Stagehand browser traces to a Braintrust project."
+---
+
+Braintrust is an eval platform that also accepts OTLP. Route Stagehand spans into the project you already use for LLM traces so a browser step and a model call can share a timeline. Then you can score the browser run the same way you score a model call.
+
+The `x-bt-parent` header is what selects that project. Without it, Braintrust does not know where to file the trace. See [Braintrust's OpenTelemetry documentation](https://www.braintrust.dev/docs/integrations/sdk-integrations/opentelemetry).
+
+## Cloud setup
+
+1. Create a Braintrust API key.
+2. Copy the ID of the project that should receive the traces. You can also route by project name with `project_name:` instead of `project_id:`.
+3. Set `BRAINTRUST_API_KEY` and `BRAINTRUST_PROJECT_ID`.
+4. US orgs use `https://api.braintrust.dev/otel/v1/traces`. EU data plane orgs use `https://api-eu.braintrust.dev/otel/v1/traces`. Set `BRAINTRUST_OTLP_TRACES_ENDPOINT` when you are not on US.
+
+## Configure Stagehand
+
+
+
+```typescript
+import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
+
+const browser = await localBrowser.launch();
+const stagehand = await Stagehand.create({
+ browser,
+ telemetry: {
+ traces: {
+ endpoint: process.env.BRAINTRUST_OTLP_TRACES_ENDPOINT ?? "https://api.braintrust.dev/otel/v1/traces",
+ headers: {
+ Authorization: `Bearer ${process.env.BRAINTRUST_API_KEY}`,
+ "x-bt-parent": `project_id:${process.env.BRAINTRUST_PROJECT_ID}`,
+ },
+ },
+ },
+});
+
+const [page] = await browser.context.pages();
+await page.goto("https://example.com");
+await stagehand.observe("find the link on the page");
+await stagehand.close();
+```
+
+
+
+```python
+import os
+from stagehand import Stagehand, TelemetryConfig, local_browser
+
+browser = await local_browser.launch()
+stagehand = await Stagehand.create(
+ browser=browser,
+ telemetry=TelemetryConfig.model_validate({
+ "traces": {
+ "endpoint": os.environ.get("BRAINTRUST_OTLP_TRACES_ENDPOINT", "https://api.braintrust.dev/otel/v1/traces"),
+ "headers": {
+ "Authorization": f"Bearer {os.environ['BRAINTRUST_API_KEY']}",
+ "x-bt-parent": f"project_id:{os.environ['BRAINTRUST_PROJECT_ID']}",
+ },
+ }
+ }),
+)
+
+page = (await browser.context.pages())[0]
+await page.goto("https://example.com")
+await stagehand.observe("find the link on the page")
+await stagehand.close()
+```
+
+
+
+```go
+package main
+
+import (
+ "context"
+ "os"
+
+ "github.com/browserbase/stagehand/packages/sdk-go"
+)
+
+func envOr(name, fallback string) string {
+ if value := os.Getenv(name); value != "" { return value }
+ return fallback
+}
+
+func main() {
+ ctx := context.Background()
+ browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
+ if err != nil { panic(err) }
+
+ client, err := stagehand.Create(ctx, stagehand.CreateOptions{
+ Browser: browser,
+ Telemetry: stagehand.TelemetryConfig{
+ Traces: stagehand.TelemetryTraces{
+ Endpoint: envOr("BRAINTRUST_OTLP_TRACES_ENDPOINT", "https://api.braintrust.dev/otel/v1/traces"),
+ Headers: stagehand.TelemetryTracesHeaders{
+ "Authorization": "Bearer " + os.Getenv("BRAINTRUST_API_KEY"),
+ "x-bt-parent": "project_id:" + os.Getenv("BRAINTRUST_PROJECT_ID"),
+ },
+ },
+ },
+ })
+ if err != nil { panic(err) }
+ defer client.Close(ctx)
+
+ browserContext, _ := browser.Context()
+ pages, _ := browserContext.Pages(ctx)
+ page := pages[0]
+ page.Goto(ctx, "https://example.com", nil)
+ instruction := "find the link on the page"
+ client.Observe(ctx, &instruction, nil)
+}
+```
+
+
+
+Call `stagehand.close()` before the process exits. Spans sit in a one-second batch. A script that returns immediately often drops the last export.
+
+## Verify traces
+
+Open the project named in `x-bt-parent`, then its tracing view. Traces are filed under that parent, so a missing or mistyped header looks like a successful export that never arrived.
+
+Stagehand does not emit Braintrust's LLM span conventions. You get `operation` spans for `act`, `observe`, and `extract`, and `log` spans for each log record, with `service.name` set to `stagehand-service-worker`. That is enough to hang a score on a browser run. It is not a reconstructed chat transcript.
+
+If the view is empty, confirm the data plane matches the host. EU orgs sending to `api.braintrust.dev` will not see the trace. Then confirm `close()` ran.
diff --git a/packages/docs/v4/observability/langfuse.mdx b/packages/docs/v4/observability/langfuse.mdx
new file mode 100644
index 000000000..43feb2152
--- /dev/null
+++ b/packages/docs/v4/observability/langfuse.mdx
@@ -0,0 +1,132 @@
+---
+title: "Langfuse"
+sidebarTitle: "Langfuse"
+description: "Send Stagehand browser traces to Langfuse."
+---
+
+Langfuse is an LLM tracing backend. Sessions, scores, and the observation table are the product. OTLP is how Stagehand gets spans into that table without a Langfuse SDK.
+
+Auth is HTTP Basic with the project's public and secret keys. The `x-langfuse-ingestion-version: 4` header sends spans to the current tracing table in real time. Leave it off and Langfuse can delay OTLP data by up to 10 minutes. See [Langfuse's OpenTelemetry documentation](https://langfuse.com/integrations/native/opentelemetry).
+
+## Cloud setup
+
+1. Copy the project's public and secret keys.
+2. Set `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY`.
+3. Set `LANGFUSE_BASE_URL` to your region. The default below is EU Cloud, `https://cloud.langfuse.com`. US is `https://us.cloud.langfuse.com`. Japan is `https://jp.cloud.langfuse.com`. HIPAA is `https://hipaa.cloud.langfuse.com`.
+
+## Configure Stagehand
+
+
+
+```typescript
+import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
+
+const browser = await localBrowser.launch();
+const stagehand = await Stagehand.create({
+ browser,
+ telemetry: {
+ traces: {
+ endpoint: `${(process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com").replace(/\/$/, "")}/api/public/otel/v1/traces`,
+ headers: {
+ Authorization: `Basic ${Buffer.from(`${process.env.LANGFUSE_PUBLIC_KEY}:${process.env.LANGFUSE_SECRET_KEY}`).toString("base64")}`,
+ "x-langfuse-ingestion-version": "4",
+ },
+ },
+ },
+});
+
+const [page] = await browser.context.pages();
+await page.goto("https://example.com");
+await stagehand.observe("find the link on the page");
+await stagehand.close();
+```
+
+
+
+```python
+import base64
+import os
+from stagehand import Stagehand, TelemetryConfig, local_browser
+
+browser = await local_browser.launch()
+stagehand = await Stagehand.create(
+ browser=browser,
+ telemetry=TelemetryConfig.model_validate({
+ "traces": {
+ "endpoint": f"{os.environ.get('LANGFUSE_BASE_URL', 'https://cloud.langfuse.com').rstrip('/')}/api/public/otel/v1/traces",
+ "headers": {
+ "Authorization": "Basic " + base64.b64encode(f"{os.environ['LANGFUSE_PUBLIC_KEY']}:{os.environ['LANGFUSE_SECRET_KEY']}".encode()).decode(),
+ "x-langfuse-ingestion-version": "4",
+ },
+ }
+ }),
+)
+
+page = (await browser.context.pages())[0]
+await page.goto("https://example.com")
+await stagehand.observe("find the link on the page")
+await stagehand.close()
+```
+
+
+
+```go
+package main
+
+import (
+ "context"
+ "encoding/base64"
+ "os"
+
+ "github.com/browserbase/stagehand/packages/sdk-go"
+)
+
+func envOr(name, fallback string) string {
+ if value := os.Getenv(name); value != "" { return value }
+ return fallback
+}
+
+func main() {
+ ctx := context.Background()
+ browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
+ if err != nil { panic(err) }
+
+ client, err := stagehand.Create(ctx, stagehand.CreateOptions{
+ Browser: browser,
+ Telemetry: stagehand.TelemetryConfig{
+ Traces: stagehand.TelemetryTraces{
+ Endpoint: envOr("LANGFUSE_BASE_URL", "https://cloud.langfuse.com") + "/api/public/otel/v1/traces",
+ Headers: stagehand.TelemetryTracesHeaders{
+ "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(os.Getenv("LANGFUSE_PUBLIC_KEY") + ":" + os.Getenv("LANGFUSE_SECRET_KEY"))),
+ "x-langfuse-ingestion-version": "4",
+ },
+ },
+ },
+ })
+ if err != nil { panic(err) }
+ defer client.Close(ctx)
+
+ browserContext, _ := browser.Context()
+ pages, _ := browserContext.Pages(ctx)
+ page := pages[0]
+ page.Goto(ctx, "https://example.com", nil)
+ instruction := "find the link on the page"
+ client.Observe(ctx, &instruction, nil)
+}
+```
+
+
+
+Call `stagehand.close()` before the process exits. Spans sit in a one-second batch. A script that returns immediately often drops the last export.
+
+## Self-hosted Langfuse
+
+Use the same project keys and headers. Set `LANGFUSE_BASE_URL` to the public URL of your Langfuse instance. Stagehand sends traces to `/api/public/otel/v1/traces`. Self-hosted needs Langfuse v3.22.0 or later for this path.
+
+## Verify traces
+
+Open the project and select Tracing. With the ingestion-version header, new spans should show up without the legacy delay.
+
+You will see `operation` spans for `act`, `observe`, and `extract`, and `log` spans for each log record. Langfuse maps `gen_ai.*` attributes into generations with token and cost fields. Stagehand does not emit those conventions, so these runs land as observations, not model generations. That is still enough to attach a session and a score to a browser pass.
+
+If Tracing is empty, wait a minute only if you omitted the version header. Otherwise check the region host, the Basic Auth encoding (`public:secret`), and that `close()` ran.
diff --git a/packages/docs/v4/observability/langsmith.mdx b/packages/docs/v4/observability/langsmith.mdx
new file mode 100644
index 000000000..ec1eae732
--- /dev/null
+++ b/packages/docs/v4/observability/langsmith.mdx
@@ -0,0 +1,127 @@
+---
+title: "LangSmith"
+sidebarTitle: "LangSmith"
+description: "Send Stagehand browser traces to LangSmith."
+---
+
+LangSmith is the tracing and eval product in the LangChain stack. Projects, datasets, and run filters are what you get. OTLP is the way to put Stagehand spans next to LangChain runs without wrapping Stagehand in a LangSmith SDK.
+
+The `Langsmith-Project` header selects the project. `x-api-key` authenticates. See [LangSmith's OpenTelemetry documentation](https://docs.langchain.com/langsmith/trace-with-opentelemetry).
+
+## Cloud setup
+
+1. Create a LangSmith API key.
+2. Set `LANGSMITH_API_KEY`.
+3. Set `LANGSMITH_PROJECT` if you do not want the `stagehand` project.
+4. US SaaS uses `https://api.smith.langchain.com/otel/v1/traces`. GCP EU is `https://eu.api.smith.langchain.com/otel/v1/traces`. GCP APAC is `https://apac.api.smith.langchain.com/otel/v1/traces`. AWS US is `https://aws.api.smith.langchain.com/otel/v1/traces`. Set `LANGSMITH_OTLP_TRACES_ENDPOINT` when you are not on default US.
+
+## Configure Stagehand
+
+
+
+```typescript
+import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
+
+const browser = await localBrowser.launch();
+const stagehand = await Stagehand.create({
+ browser,
+ telemetry: {
+ traces: {
+ endpoint: process.env.LANGSMITH_OTLP_TRACES_ENDPOINT ?? "https://api.smith.langchain.com/otel/v1/traces",
+ headers: {
+ "x-api-key": process.env.LANGSMITH_API_KEY!,
+ "Langsmith-Project": process.env.LANGSMITH_PROJECT ?? "stagehand",
+ },
+ },
+ },
+});
+
+const [page] = await browser.context.pages();
+await page.goto("https://example.com");
+await stagehand.observe("find the link on the page");
+await stagehand.close();
+```
+
+
+
+```python
+import os
+from stagehand import Stagehand, TelemetryConfig, local_browser
+
+browser = await local_browser.launch()
+stagehand = await Stagehand.create(
+ browser=browser,
+ telemetry=TelemetryConfig.model_validate({
+ "traces": {
+ "endpoint": os.environ.get("LANGSMITH_OTLP_TRACES_ENDPOINT", "https://api.smith.langchain.com/otel/v1/traces"),
+ "headers": {
+ "x-api-key": os.environ["LANGSMITH_API_KEY"],
+ "Langsmith-Project": os.environ.get("LANGSMITH_PROJECT", "stagehand"),
+ },
+ }
+ }),
+)
+
+page = (await browser.context.pages())[0]
+await page.goto("https://example.com")
+await stagehand.observe("find the link on the page")
+await stagehand.close()
+```
+
+
+
+```go
+package main
+
+import (
+ "context"
+ "os"
+
+ "github.com/browserbase/stagehand/packages/sdk-go"
+)
+
+func envOr(name, fallback string) string {
+ if value := os.Getenv(name); value != "" { return value }
+ return fallback
+}
+
+func main() {
+ ctx := context.Background()
+ browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
+ if err != nil { panic(err) }
+
+ client, err := stagehand.Create(ctx, stagehand.CreateOptions{
+ Browser: browser,
+ Telemetry: stagehand.TelemetryConfig{
+ Traces: stagehand.TelemetryTraces{
+ Endpoint: envOr("LANGSMITH_OTLP_TRACES_ENDPOINT", "https://api.smith.langchain.com/otel/v1/traces"),
+ Headers: stagehand.TelemetryTracesHeaders{
+ "x-api-key": os.Getenv("LANGSMITH_API_KEY"),
+ "Langsmith-Project": envOr("LANGSMITH_PROJECT", "stagehand"),
+ },
+ },
+ },
+ })
+ if err != nil { panic(err) }
+ defer client.Close(ctx)
+
+ browserContext, _ := browser.Context()
+ pages, _ := browserContext.Pages(ctx)
+ page := pages[0]
+ page.Goto(ctx, "https://example.com", nil)
+ instruction := "find the link on the page"
+ client.Observe(ctx, &instruction, nil)
+}
+```
+
+
+
+Call `stagehand.close()` before the process exits. Spans sit in a one-second batch. A script that returns immediately often drops the last export.
+
+## Verify traces
+
+Open the project named in `Langsmith-Project` and select Traces. Switch the view to Runs if you want each span as a row instead of the trace tree.
+
+Stagehand sets `service.name` to `stagehand-service-worker`. Operation spans wrap `act`, `observe`, and `extract`. Log records are child runs with `stagehand.span.type` set to `log`. LangSmith's richer mapping (`langsmith.span.kind`, `gen_ai.*`) is optional. We do not send those attributes, so these runs look like generic spans you can still tag and score.
+
+If the project is empty, confirm the regional host. A US key against `eu.api.smith.langchain.com` will not show the run. Then confirm the project name matches exactly and that `close()` ran.
diff --git a/packages/docs/v4/observability/otel-collector.mdx b/packages/docs/v4/observability/otel-collector.mdx
new file mode 100644
index 000000000..d5f5b4261
--- /dev/null
+++ b/packages/docs/v4/observability/otel-collector.mdx
@@ -0,0 +1,70 @@
+---
+title: "OpenTelemetry Collector"
+sidebarTitle: "OTel Collector"
+description: "Route Stagehand traces through an OpenTelemetry Collector."
+---
+
+Use a collector when one Stagehand process needs to reach more than one backend, or when a backend wants a different encoding than Stagehand sends. Stagehand's runtime exporter posts OTLP/HTTP JSON to a single `/v1/traces` URL. Direct export is simpler for one supported provider.
+
+A collector is the right extra hop if you need to batch, sample, or transcode. It can receive Stagehand's JSON and export protobuf when a backend requires that encoding.
+
+## Configure Stagehand
+
+Point Stagehand at the collector's OTLP/HTTP receiver. Default is port 4318.
+
+```typescript
+import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
+
+const browser = await localBrowser.launch();
+const stagehand = await Stagehand.create({
+ browser,
+ telemetry: {
+ traces: {
+ endpoint: "http://localhost:4318/v1/traces",
+ headers: {},
+ },
+ },
+});
+```
+
+Call `stagehand.close()` before the process exits. Spans sit in a one-second batch. The collector cannot export a batch that never arrived.
+
+## Configure the collector
+
+This example receives OTLP/HTTP from Stagehand and forwards it to two providers. Copy each exporter's endpoint and headers from that provider's page. `otlphttp` exporter `endpoint` values are the base host. The exporter appends `/v1/traces` itself, so do not include the path twice.
+
+```yaml
+receivers:
+ otlp:
+ protocols:
+ http:
+ endpoint: 0.0.0.0:4318
+
+processors:
+ batch: {}
+
+exporters:
+ otlphttp/first:
+ endpoint: ${FIRST_OTLP_ENDPOINT}
+ headers:
+ Authorization: ${FIRST_OTLP_AUTHORIZATION}
+ otlphttp/second:
+ endpoint: ${SECOND_OTLP_ENDPOINT}
+ headers:
+ Authorization: ${SECOND_OTLP_AUTHORIZATION}
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [otlphttp/first, otlphttp/second]
+```
+
+Some providers use a header other than `Authorization`. Braintrust wants `x-bt-parent`. LangSmith wants `x-api-key` and `Langsmith-Project`. Sentry wants `x-sentry-auth`. Put those on the matching exporter.
+
+## Verify traces
+
+Start the collector before Stagehand. Watch the collector logs for export errors first. A 404 on an exporter usually means the endpoint already included `/v1/traces` and the exporter appended it again. A 401 is a header name or credential. Then confirm the run in each provider UI, filtering on `service.name = stagehand-service-worker`.
+
+If only one backend shows the trace, the pipeline is fine and the other exporter's host or header is wrong. If neither shows it, Stagehand never reached the collector. Confirm port 4318 and that `close()` ran.
diff --git a/packages/docs/v4/observability/overview.mdx b/packages/docs/v4/observability/overview.mdx
new file mode 100644
index 000000000..b8083e40d
--- /dev/null
+++ b/packages/docs/v4/observability/overview.mdx
@@ -0,0 +1,41 @@
+---
+title: "Observability providers"
+sidebarTitle: "Overview"
+description: "Send Stagehand traces to supported OpenTelemetry providers."
+---
+
+Stagehand emits OpenTelemetry spans for every `act`, `observe`, and `extract` call, and for every log record. You point `telemetry.traces` at an OTLP/HTTP endpoint that ends in `/v1/traces`. The pages below have the host and headers for each backend.
+
+The exporter batches spans and flushes on `stagehand.close()`. Skip that in a short script and the last batch never leaves the process.
+
+## What a Stagehand trace looks like
+
+Every export sets `service.name` to `stagehand-service-worker` and `service.namespace` to `browserbase`. Sampling is 100%.
+
+**Operation spans.** One span wraps each `act`, `observe`, or `extract` call. `stagehand.span.type` is `operation`. The payload is JSON on `stagehand.span.data`. Failures set error status and record the exception.
+
+**Log spans.** Each log record is its own span, not a span event. `stagehand.span.type` is `log`. The message is on `stagehand.log.message`, the level on `stagehand.log.level`, and the structured payload on `stagehand.log.data`.
+
+Configure the endpoint in [`Stagehand.create()`](/v4/configuration/observability#tracing). Then copy the provider's host and auth headers from its page.
+
+## Choose a provider
+
+These backends all speak OTLP. They do not all do the same job with the spans once they arrive.
+
+
+
+ Score browser runs in the same project as your LLM evals.
+
+
+ LLM tracing with sessions, scores, and a v4 observation table.
+
+
+ Project-scoped traces and evals in the LangChain stack.
+
+
+ Tie Stagehand failures to the same project as your application errors.
+
+
+ Fan out one Stagehand export to more than one backend, or transcode JSON OTLP to protobuf.
+
+
diff --git a/packages/docs/v4/observability/sentry.mdx b/packages/docs/v4/observability/sentry.mdx
new file mode 100644
index 000000000..6924fbd32
--- /dev/null
+++ b/packages/docs/v4/observability/sentry.mdx
@@ -0,0 +1,118 @@
+---
+title: "Sentry"
+sidebarTitle: "Sentry"
+description: "Send Stagehand browser traces directly to Sentry."
+---
+
+Sentry traces exist to sit next to the errors you already file in a project. Direct OTLP is how Stagehand gets into that same trace view without the Sentry SDK.
+
+Direct OTLP tracing is in open beta. Copy the traces URL and public key from the project. Do not derive the URL from the DSN. See [Sentry's OpenTelemetry documentation](https://docs.sentry.io/concepts/otlp/direct/traces/).
+
+## Cloud setup
+
+1. Open Project settings > Client Keys (DSN), then the project's OpenTelemetry settings.
+2. Copy the OTLP traces URL and public key.
+3. Set `SENTRY_OTLP_TRACES_ENDPOINT` and `SENTRY_PUBLIC_KEY`.
+
+## Configure Stagehand
+
+
+
+```typescript
+import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
+
+const browser = await localBrowser.launch();
+const stagehand = await Stagehand.create({
+ browser,
+ telemetry: {
+ traces: {
+ endpoint: process.env.SENTRY_OTLP_TRACES_ENDPOINT!,
+ headers: {
+ "x-sentry-auth": `sentry sentry_key=${process.env.SENTRY_PUBLIC_KEY}`,
+ },
+ },
+ },
+});
+
+const [page] = await browser.context.pages();
+await page.goto("https://example.com");
+await stagehand.observe("find the link on the page");
+await stagehand.close();
+```
+
+
+
+```python
+import os
+from stagehand import Stagehand, TelemetryConfig, local_browser
+
+browser = await local_browser.launch()
+stagehand = await Stagehand.create(
+ browser=browser,
+ telemetry=TelemetryConfig.model_validate({
+ "traces": {
+ "endpoint": os.environ["SENTRY_OTLP_TRACES_ENDPOINT"],
+ "headers": {
+ "x-sentry-auth": f"sentry sentry_key={os.environ['SENTRY_PUBLIC_KEY']}",
+ },
+ }
+ }),
+)
+
+page = (await browser.context.pages())[0]
+await page.goto("https://example.com")
+await stagehand.observe("find the link on the page")
+await stagehand.close()
+```
+
+
+
+```go
+package main
+
+import (
+ "context"
+ "os"
+
+ "github.com/browserbase/stagehand/packages/sdk-go"
+)
+
+func main() {
+ ctx := context.Background()
+ browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
+ if err != nil { panic(err) }
+
+ client, err := stagehand.Create(ctx, stagehand.CreateOptions{
+ Browser: browser,
+ Telemetry: stagehand.TelemetryConfig{
+ Traces: stagehand.TelemetryTraces{
+ Endpoint: os.Getenv("SENTRY_OTLP_TRACES_ENDPOINT"),
+ Headers: stagehand.TelemetryTracesHeaders{
+ "x-sentry-auth": "sentry sentry_key=" + os.Getenv("SENTRY_PUBLIC_KEY"),
+ },
+ },
+ },
+ })
+ if err != nil { panic(err) }
+ defer client.Close(ctx)
+
+ browserContext, _ := browser.Context()
+ pages, _ := browserContext.Pages(ctx)
+ page := pages[0]
+ page.Goto(ctx, "https://example.com", nil)
+ instruction := "find the link on the page"
+ client.Observe(ctx, &instruction, nil)
+}
+```
+
+
+
+Call `stagehand.close()` before the process exits. Spans sit in a one-second batch. A script that returns immediately often drops the last export.
+
+## Verify traces
+
+Open Explore, select Traces, and filter to the time of your run. Look for `stagehand-service-worker`.
+
+A failed `act` / `observe` / `extract` sets error status on the operation span and records the exception. That is the path into Sentry Issues. Log records are their own spans (`stagehand.span.type = log`), not OpenTelemetry span events. Sentry drops span events on ingest, so this distinction matters. You still get the log spans in the trace view.
+
+Sentry ingests array attributes and span links but does not let you search or aggregate them. Filter on `service.name` and span name. If Explore is empty, the usual causes are a URL copied from the DSN instead of the OTLP settings, a stale public key, or a process that exited before `close()`.