From f742c845c9504f31a0868ca5d4c71de0f74cf803 Mon Sep 17 00:00:00 2001 From: Vishal Anton Date: Fri, 11 Sep 2026 11:38:02 +0530 Subject: [PATCH 1/2] docs: Add observability providers --- packages/docs/docs.json | 16 ++ packages/docs/images/observability/axiom.svg | 1 + .../docs/images/observability/braintrust.svg | 5 + .../docs/images/observability/honeycomb.svg | 95 ++++++++++ .../docs/images/observability/hyperdx.svg | 12 ++ .../docs/images/observability/laminar.svg | 9 + .../docs/images/observability/langfuse.svg | 11 ++ .../images/observability/opentelemetry.svg | 4 + packages/docs/images/observability/sentry.svg | 1 + packages/docs/images/observability/signoz.svg | 1 + .../docs/v4/configuration/observability.mdx | 4 + packages/docs/v4/observability/axiom.mdx | 136 +++++++++++++++ packages/docs/v4/observability/braintrust.mdx | 127 ++++++++++++++ packages/docs/v4/observability/honeycomb.mdx | 132 ++++++++++++++ packages/docs/v4/observability/hyperdx.mdx | 163 ++++++++++++++++++ packages/docs/v4/observability/laminar.mdx | 119 +++++++++++++ packages/docs/v4/observability/langfuse.mdx | 132 ++++++++++++++ packages/docs/v4/observability/langsmith.mdx | 127 ++++++++++++++ .../docs/v4/observability/otel-collector.mdx | 70 ++++++++ packages/docs/v4/observability/overview.mdx | 57 ++++++ packages/docs/v4/observability/sentry.mdx | 118 +++++++++++++ packages/docs/v4/observability/signoz.mdx | 161 +++++++++++++++++ 22 files changed, 1501 insertions(+) create mode 100644 packages/docs/images/observability/axiom.svg create mode 100644 packages/docs/images/observability/braintrust.svg create mode 100644 packages/docs/images/observability/honeycomb.svg create mode 100644 packages/docs/images/observability/hyperdx.svg create mode 100644 packages/docs/images/observability/laminar.svg create mode 100644 packages/docs/images/observability/langfuse.svg create mode 100644 packages/docs/images/observability/opentelemetry.svg create mode 100644 packages/docs/images/observability/sentry.svg create mode 100644 packages/docs/images/observability/signoz.svg create mode 100644 packages/docs/v4/observability/axiom.mdx create mode 100644 packages/docs/v4/observability/braintrust.mdx create mode 100644 packages/docs/v4/observability/honeycomb.mdx create mode 100644 packages/docs/v4/observability/hyperdx.mdx create mode 100644 packages/docs/v4/observability/laminar.mdx create mode 100644 packages/docs/v4/observability/langfuse.mdx create mode 100644 packages/docs/v4/observability/langsmith.mdx create mode 100644 packages/docs/v4/observability/otel-collector.mdx create mode 100644 packages/docs/v4/observability/overview.mdx create mode 100644 packages/docs/v4/observability/sentry.mdx create mode 100644 packages/docs/v4/observability/signoz.mdx diff --git a/packages/docs/docs.json b/packages/docs/docs.json index a60d3d068d..3a6a8ea18a 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -64,6 +64,22 @@ "v4/integrations/vercel-ai-sdk" ] }, + { + "group": "Observability providers", + "pages": [ + "v4/observability/overview", + "v4/observability/axiom", + "v4/observability/braintrust", + "v4/observability/honeycomb", + "v4/observability/hyperdx", + "v4/observability/laminar", + "v4/observability/langfuse", + "v4/observability/langsmith", + "v4/observability/sentry", + "v4/observability/signoz", + "v4/observability/otel-collector" + ] + }, { "group": "Best practices", "pages": [ diff --git a/packages/docs/images/observability/axiom.svg b/packages/docs/images/observability/axiom.svg new file mode 100644 index 0000000000..ed4b570572 --- /dev/null +++ b/packages/docs/images/observability/axiom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/images/observability/braintrust.svg b/packages/docs/images/observability/braintrust.svg new file mode 100644 index 0000000000..8665a12bf7 --- /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/honeycomb.svg b/packages/docs/images/observability/honeycomb.svg new file mode 100644 index 0000000000..34ee91e611 --- /dev/null +++ b/packages/docs/images/observability/honeycomb.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/images/observability/hyperdx.svg b/packages/docs/images/observability/hyperdx.svg new file mode 100644 index 0000000000..c0ac90f722 --- /dev/null +++ b/packages/docs/images/observability/hyperdx.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/images/observability/laminar.svg b/packages/docs/images/observability/laminar.svg new file mode 100644 index 0000000000..f80d1cf885 --- /dev/null +++ b/packages/docs/images/observability/laminar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/docs/images/observability/langfuse.svg b/packages/docs/images/observability/langfuse.svg new file mode 100644 index 0000000000..313290438a --- /dev/null +++ b/packages/docs/images/observability/langfuse.svg @@ -0,0 +1,11 @@ + + Langfuse + + + + \ 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 0000000000..d7c352f83b --- /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 0000000000..250601867c --- /dev/null +++ b/packages/docs/images/observability/sentry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/images/observability/signoz.svg b/packages/docs/images/observability/signoz.svg new file mode 100644 index 0000000000..641f7bd37a --- /dev/null +++ b/packages/docs/images/observability/signoz.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 3cd4b353e6..36866f23c7 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/axiom.mdx b/packages/docs/v4/observability/axiom.mdx new file mode 100644 index 0000000000..21c5194d76 --- /dev/null +++ b/packages/docs/v4/observability/axiom.mdx @@ -0,0 +1,136 @@ +--- +title: "Axiom" +sidebarTitle: "Axiom" +description: "Query Stagehand traces as events in an Axiom dataset." +--- + +Axiom stores traces as events in a dataset and you query them with APL, the same language you already use for logs. There is no separate APM product. The dataset is the product. If browser logs already live in Axiom, Stagehand spans sit in the same kind of table. + +Ingest goes to your organization's edge domain, not `api.axiom.co`. See [Axiom's OpenTelemetry documentation](https://axiom.co/docs/send-data/opentelemetry) and [edge deployments](https://axiom.co/docs/reference/edge-deployments). + +## Cloud setup + +1. Create a dataset for traces, such as `stagehand-traces`. Axiom wants a dedicated dataset per OpenTelemetry signal. Do not send these spans to a metrics dataset. +2. Create an API token with ingest access to that dataset. Personal access tokens cannot ingest. +3. Set `AXIOM_API_TOKEN`, `AXIOM_DATASET`, and `AXIOM_DOMAIN`. `AXIOM_DOMAIN` is the ingest host from Settings > General. US East 1 is `us-east-1.aws.edge.axiom.co`. EU Central 1 is `eu-central-1.aws.edge.axiom.co`. + +## Configure Stagehand + + + +```typescript +import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; + +const browser = await localBrowser.launch(); +const stagehand = await Stagehand.create({ + browser, + telemetry: { + traces: { + endpoint: `https://${process.env.AXIOM_DOMAIN ?? "us-east-1.aws.edge.axiom.co"}/v1/traces`, + headers: { + Authorization: `Bearer ${process.env.AXIOM_API_TOKEN}`, + "X-Axiom-Dataset": process.env.AXIOM_DATASET!, + }, + }, + }, +}); + +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": f"https://{os.environ.get('AXIOM_DOMAIN', 'us-east-1.aws.edge.axiom.co')}/v1/traces", + "headers": { + "Authorization": f"Bearer {os.environ['AXIOM_API_TOKEN']}", + "X-Axiom-Dataset": os.environ["AXIOM_DATASET"], + }, + } + }), +) + +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: "https://" + envOr("AXIOM_DOMAIN", "us-east-1.aws.edge.axiom.co") + "/v1/traces", + Headers: stagehand.TelemetryTracesHeaders{ + "Authorization": "Bearer " + os.Getenv("AXIOM_API_TOKEN"), + "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET"), + }, + }, + }, + }) + 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 dataset and go to Stream. Filter on `['service.name'] == "stagehand-service-worker"`. + +You should see `operation` spans for `observe` (and `act` / `extract` when you call those), plus a `log` span for each log record. The type is on `stagehand.span.type`. Operation payloads are JSON on `stagehand.span.data`. + +A starting query. Replace the dataset name if yours is different. + +```apl +['stagehand-traces'] +| where ['service.name'] == "stagehand-service-worker" +| where ['stagehand.span.type'] == "operation" +``` + +If Stream is empty, check the host first. `api.axiom.co` is the management API. OTLP ingest uses the edge domain, and a mismatched region returns 400. Then confirm the token can ingest to this dataset and that you called `close()`. diff --git a/packages/docs/v4/observability/braintrust.mdx b/packages/docs/v4/observability/braintrust.mdx new file mode 100644 index 0000000000..5393605c3f --- /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/honeycomb.mdx b/packages/docs/v4/observability/honeycomb.mdx new file mode 100644 index 0000000000..e86c8ea590 --- /dev/null +++ b/packages/docs/v4/observability/honeycomb.mdx @@ -0,0 +1,132 @@ +--- +title: "Honeycomb" +sidebarTitle: "Honeycomb" +description: "Query Stagehand browser traces in Honeycomb." +--- + +Honeycomb is built for asking questions of high-cardinality span attributes. You do not pick dimensions in advance. Filter on `service.name`, break down on `stagehand.span.type`, and heat-map duration when an `observe` call is slow. + +See [Honeycomb's OpenTelemetry documentation](https://docs.honeycomb.io/send-data/opentelemetry/). + +## Cloud setup + +1. Create an environment or select an existing one. Current Honeycomb routes by environment, not by a dataset header. `x-honeycomb-dataset` is only for Honeycomb Classic. +2. Create an ingest API key and set `HONEYCOMB_API_KEY`. +3. US accounts use `https://api.honeycomb.io/v1/traces`. EU accounts use `https://api.eu1.honeycomb.io/v1/traces`. Set `HONEYCOMB_OTLP_TRACES_ENDPOINT` when you are on EU. + +## 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.HONEYCOMB_OTLP_TRACES_ENDPOINT ?? "https://api.honeycomb.io/v1/traces", + headers: { + "x-honeycomb-team": process.env.HONEYCOMB_API_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.get("HONEYCOMB_OTLP_TRACES_ENDPOINT", "https://api.honeycomb.io/v1/traces"), + "headers": { + "x-honeycomb-team": os.environ["HONEYCOMB_API_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 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("HONEYCOMB_OTLP_TRACES_ENDPOINT", "https://api.honeycomb.io/v1/traces"), + Headers: stagehand.TelemetryTracesHeaders{ + "x-honeycomb-team": os.Getenv("HONEYCOMB_API_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 Query in the environment that owns the ingest key. Filter `service.name = stagehand-service-worker`. + +Honeycomb indexes the attributes on each span, so you can break down on `stagehand.span.type` without creating a schema. Operation spans wrap `act`, `observe`, and `extract`. Log spans are the individual log records, with the text on `stagehand.log.message`. + +A first query: + +``` +service.name = stagehand-service-worker +AND stagehand.span.type = operation +``` + +Group by `name` and look at `HEATMAP(duration_ms)` when an observe call is slow. BubbleUp is useful once you have more than a handful of runs. + +A US key against `api.eu1.honeycomb.io` is rejected. If Query is empty after a short script, `close()` probably never flushed the batch. diff --git a/packages/docs/v4/observability/hyperdx.mdx b/packages/docs/v4/observability/hyperdx.mdx new file mode 100644 index 0000000000..75c74f7e77 --- /dev/null +++ b/packages/docs/v4/observability/hyperdx.mdx @@ -0,0 +1,163 @@ +--- +title: "HyperDX" +sidebarTitle: "HyperDX" +description: "Search Stagehand traces and logs in HyperDX." +--- + +HyperDX is a ClickHouse-backed search UI for traces, logs, and metrics in one place. You jump from a slow span to the log line next to it without stitching timestamps by hand. Send logs to the same instance if you already tail them there. + +See [HyperDX's OpenTelemetry documentation](https://www.hyperdx.io/docs/install/opentelemetry). The HTTP ingest host is `https://in-otel.hyperdx.io`. Auth is the raw ingestion key in `Authorization`. Do not prefix `Bearer`. + +## Cloud setup + +1. Copy the ingestion key from Team Settings. +2. Set `HYPERDX_API_KEY`. + +## Configure Stagehand + + + +```typescript +import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; + +const browser = await localBrowser.launch(); +const stagehand = await Stagehand.create({ + browser, + telemetry: { + traces: { + endpoint: "https://in-otel.hyperdx.io/v1/traces", + headers: { + Authorization: process.env.HYPERDX_API_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": "https://in-otel.hyperdx.io/v1/traces", + "headers": { + "Authorization": os.environ["HYPERDX_API_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: "https://in-otel.hyperdx.io/v1/traces", + Headers: stagehand.TelemetryTracesHeaders{ + "Authorization": os.Getenv("HYPERDX_API_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. + +## HyperDX Local + +Run HyperDX Local when you want the same search UI on your machine. The UI is `http://localhost:8080`. OTLP/HTTP is `http://localhost:4318`. The local receiver does not validate the key. HyperDX-maintained SDKs still want a non-empty value. Stagehand does not, so empty headers are fine. + +```bash +docker run -p 8000:8000 -p 4318:4318 -p 4317:4317 -p 8080:8080 -p 8002:8002 docker.hyperdx.io/hyperdx/hyperdx-local +``` + + + +```typescript +const stagehand = await Stagehand.create({ + browser, + telemetry: { + traces: { + endpoint: "http://localhost:4318/v1/traces", + headers: {}, + }, + }, +}); +``` + + +```python +stagehand = await Stagehand.create( + browser=browser, + telemetry=TelemetryConfig.model_validate({ + "traces": {"endpoint": "http://localhost:4318/v1/traces", "headers": {}}, + }), +) +``` + + +```go +client, err := stagehand.Create(ctx, stagehand.CreateOptions{ + Browser: browser, + Telemetry: stagehand.TelemetryConfig{ + Traces: stagehand.TelemetryTraces{ + Endpoint: "http://localhost:4318/v1/traces", + }, + }, +}) +``` + + + +## Verify traces + +Open search and select the trace source. Filter for `stagehand-service-worker`. + +You should see an `operation` span for `observe`, with `log` spans under the same trace. HyperDX's value is clicking from that operation into any log line that landed at the same time, so send logs to the same instance if you have them. + +On Local, use `http://localhost:8080`. If search is empty, confirm the container mapped 4318 and that the process called `close()`. A `Bearer ` prefix on the cloud `Authorization` header is a common 401. diff --git a/packages/docs/v4/observability/laminar.mdx b/packages/docs/v4/observability/laminar.mdx new file mode 100644 index 0000000000..2910583fc5 --- /dev/null +++ b/packages/docs/v4/observability/laminar.mdx @@ -0,0 +1,119 @@ +--- +title: "Laminar" +sidebarTitle: "Laminar" +description: "Send Stagehand browser traces to Laminar." +--- + +Laminar is an OpenTelemetry-native observability product for agents. It turns spans into a transcript you can read, then lets you run SQL over those traces and attach signals. That is a different job from a generic APM. + +Stagehand v4 talks to Laminar over OTLP. You get the `act` / `observe` / `extract` tree and the log spans. Laminar also publishes a native SDK wrapper for Stagehand v2 and v3 that records the browser session and LLM calls. That wrapper is a different integration than this page. + +See [Laminar's OpenTelemetry troubleshooting](https://laminar.sh/docs/tracing/troubleshooting-opentelemetry). Laminar prefers gRPC. Stagehand exports OTLP/HTTP, so use the HTTP traces URL on port 443. + +## Cloud setup + +1. Create a project API key in Laminar. +2. Set `LMNR_PROJECT_API_KEY`. + +## Configure Stagehand + + + +```typescript +import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; + +const browser = await localBrowser.launch(); +const stagehand = await Stagehand.create({ + browser, + telemetry: { + traces: { + endpoint: "https://api.lmnr.ai:443/v1/traces", + headers: { + Authorization: `Bearer ${process.env.LMNR_PROJECT_API_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": "https://api.lmnr.ai:443/v1/traces", + "headers": { + "Authorization": f"Bearer {os.environ['LMNR_PROJECT_API_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: "https://api.lmnr.ai:443/v1/traces", + Headers: stagehand.TelemetryTracesHeaders{ + "Authorization": "Bearer " + os.Getenv("LMNR_PROJECT_API_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 the Laminar project and select Traces. Filter for `stagehand-service-worker`. You should see an `operation` span for `observe`, with `log` spans on the same trace. Laminar will try to render that tree as a transcript. Stagehand's OTLP payload is operation and log attributes, not `gen_ai.*` conventions, so expect a span tree first and a reconstructed chat second. + +Laminar documents that its HTTP endpoint expects protobuf, not JSON. Stagehand's runtime exporter sends OTLP/HTTP JSON. If traces never appear or the exporter logs a 500, send them through an [OpenTelemetry Collector](/v4/observability/otel-collector) that forwards protobuf to `https://api.lmnr.ai:443`. A 404 usually means the path is missing `/v1/traces`. Hitting the gRPC port with HTTP produces a parse error. + +The native v2/v3 wrapper is the path that also captures the session recording. This OTLP setup does not. diff --git a/packages/docs/v4/observability/langfuse.mdx b/packages/docs/v4/observability/langfuse.mdx new file mode 100644 index 0000000000..43feb2152f --- /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 0000000000..ec1eae732d --- /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 0000000000..d9435f4302 --- /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. Laminar's HTTP endpoint is the case that comes up most. It expects protobuf. The collector can receive Stagehand's JSON and export protobuf. + +## 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`. Honeycomb wants `x-honeycomb-team`. 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 0000000000..390a2c8789 --- /dev/null +++ b/packages/docs/v4/observability/overview.mdx @@ -0,0 +1,57 @@ +--- +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. + + + + APL queries over Stagehand spans in a dataset, next to your logs. + + + Score browser runs in the same project as your LLM evals. + + + High-cardinality queries over span attributes, no schema first. + + + Search traces and logs in one ClickHouse-backed UI. + + + Agent transcripts and SQL over OTLP spans from browser runs. + + + 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. + + + Open-source APM. Traces, metrics, and logs in one regional endpoint. + + + 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 0000000000..6924fbd32b --- /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()`. diff --git a/packages/docs/v4/observability/signoz.mdx b/packages/docs/v4/observability/signoz.mdx new file mode 100644 index 0000000000..322b76cdc3 --- /dev/null +++ b/packages/docs/v4/observability/signoz.mdx @@ -0,0 +1,161 @@ +--- +title: "SigNoz" +sidebarTitle: "SigNoz" +description: "Send Stagehand browser traces to SigNoz." +--- + +SigNoz is an OpenTelemetry-first APM. Traces, metrics, and logs share one backend. The trace view is a service map plus a span waterfall, which is what you want when Stagehand is one more service next to the ones you already watch there. + +Cloud ingest is regional. Self-hosted exposes the usual collector port `4318` and does not need an ingestion key. See [SigNoz Cloud ingestion](https://signoz.io/docs/ingestion/signoz-cloud/overview/). + +## Cloud setup + +1. Open Settings > Ingestion and copy the regional traces endpoint and ingestion key. +2. Set `SIGNOZ_OTLP_TRACES_ENDPOINT` and `SIGNOZ_INGESTION_KEY`. The traces URL looks like `https://ingest..signoz.cloud:443/v1/traces`. Cloud uses port 443 for both HTTP and gRPC, not 4318. + +## 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.SIGNOZ_OTLP_TRACES_ENDPOINT!, + headers: { + "signoz-ingestion-key": process.env.SIGNOZ_INGESTION_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["SIGNOZ_OTLP_TRACES_ENDPOINT"], + "headers": { + "signoz-ingestion-key": os.environ["SIGNOZ_INGESTION_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("SIGNOZ_OTLP_TRACES_ENDPOINT"), + Headers: stagehand.TelemetryTracesHeaders{ + "signoz-ingestion-key": os.Getenv("SIGNOZ_INGESTION_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. + +`signoz-access-token` and `Authorization: Bearer ` are deprecated on Cloud. Use `signoz-ingestion-key`. + +## Self-hosted SigNoz + +Point Stagehand at the OTLP/HTTP receiver on your SigNoz collector. The default local installation does not require an ingestion header. + + + +```typescript +const stagehand = await Stagehand.create({ + browser, + telemetry: { + traces: { + endpoint: "http://localhost:4318/v1/traces", + headers: {}, + }, + }, +}); +``` + + +```python +stagehand = await Stagehand.create( + browser=browser, + telemetry=TelemetryConfig.model_validate({ + "traces": {"endpoint": "http://localhost:4318/v1/traces", "headers": {}}, + }), +) +``` + + +```go +client, err := stagehand.Create(ctx, stagehand.CreateOptions{ + Browser: browser, + Telemetry: stagehand.TelemetryConfig{ + Traces: stagehand.TelemetryTraces{ + Endpoint: "http://localhost:4318/v1/traces", + }, + }, +}) +``` + + + +## Verify traces + +Open Traces and filter for `service.name = stagehand-service-worker`. Open a trace. You should see an `operation` span for `observe`, with `log` spans underneath. + +SigNoz is useful once Stagehand is one service among others. The waterfall shows how long the browser step sat relative to the handler that called it. For a lone local script, the same view is a short tree with one operation and some logs, which is enough to confirm ingest. + +If Cloud shows nothing, the usual miss is the region in the host (`ingest..signoz.cloud`) or sending to 4318 instead of 443. Payloads over 16 MB are rejected. Then confirm `close()` ran. From e6f8fb87807517915a9af4eb7e74e2daeed06617 Mon Sep 17 00:00:00 2001 From: Vishal Anton Date: Wed, 16 Sep 2026 02:11:24 +0530 Subject: [PATCH 2/2] docs: narrow observability provider guides --- packages/docs/docs.json | 5 - packages/docs/images/observability/axiom.svg | 1 - .../docs/images/observability/honeycomb.svg | 95 ---------- .../docs/images/observability/hyperdx.svg | 12 -- .../docs/images/observability/laminar.svg | 9 - packages/docs/images/observability/signoz.svg | 1 - packages/docs/v4/observability/axiom.mdx | 136 --------------- packages/docs/v4/observability/honeycomb.mdx | 132 -------------- packages/docs/v4/observability/hyperdx.mdx | 163 ------------------ packages/docs/v4/observability/laminar.mdx | 119 ------------- .../docs/v4/observability/otel-collector.mdx | 4 +- packages/docs/v4/observability/overview.mdx | 18 +- packages/docs/v4/observability/signoz.mdx | 161 ----------------- 13 files changed, 3 insertions(+), 853 deletions(-) delete mode 100644 packages/docs/images/observability/axiom.svg delete mode 100644 packages/docs/images/observability/honeycomb.svg delete mode 100644 packages/docs/images/observability/hyperdx.svg delete mode 100644 packages/docs/images/observability/laminar.svg delete mode 100644 packages/docs/images/observability/signoz.svg delete mode 100644 packages/docs/v4/observability/axiom.mdx delete mode 100644 packages/docs/v4/observability/honeycomb.mdx delete mode 100644 packages/docs/v4/observability/hyperdx.mdx delete mode 100644 packages/docs/v4/observability/laminar.mdx delete mode 100644 packages/docs/v4/observability/signoz.mdx diff --git a/packages/docs/docs.json b/packages/docs/docs.json index 3a6a8ea18a..29e67d2270 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -68,15 +68,10 @@ "group": "Observability providers", "pages": [ "v4/observability/overview", - "v4/observability/axiom", "v4/observability/braintrust", - "v4/observability/honeycomb", - "v4/observability/hyperdx", - "v4/observability/laminar", "v4/observability/langfuse", "v4/observability/langsmith", "v4/observability/sentry", - "v4/observability/signoz", "v4/observability/otel-collector" ] }, diff --git a/packages/docs/images/observability/axiom.svg b/packages/docs/images/observability/axiom.svg deleted file mode 100644 index ed4b570572..0000000000 --- a/packages/docs/images/observability/axiom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/docs/images/observability/honeycomb.svg b/packages/docs/images/observability/honeycomb.svg deleted file mode 100644 index 34ee91e611..0000000000 --- a/packages/docs/images/observability/honeycomb.svg +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/docs/images/observability/hyperdx.svg b/packages/docs/images/observability/hyperdx.svg deleted file mode 100644 index c0ac90f722..0000000000 --- a/packages/docs/images/observability/hyperdx.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/packages/docs/images/observability/laminar.svg b/packages/docs/images/observability/laminar.svg deleted file mode 100644 index f80d1cf885..0000000000 --- a/packages/docs/images/observability/laminar.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/packages/docs/images/observability/signoz.svg b/packages/docs/images/observability/signoz.svg deleted file mode 100644 index 641f7bd37a..0000000000 --- a/packages/docs/images/observability/signoz.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/docs/v4/observability/axiom.mdx b/packages/docs/v4/observability/axiom.mdx deleted file mode 100644 index 21c5194d76..0000000000 --- a/packages/docs/v4/observability/axiom.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: "Axiom" -sidebarTitle: "Axiom" -description: "Query Stagehand traces as events in an Axiom dataset." ---- - -Axiom stores traces as events in a dataset and you query them with APL, the same language you already use for logs. There is no separate APM product. The dataset is the product. If browser logs already live in Axiom, Stagehand spans sit in the same kind of table. - -Ingest goes to your organization's edge domain, not `api.axiom.co`. See [Axiom's OpenTelemetry documentation](https://axiom.co/docs/send-data/opentelemetry) and [edge deployments](https://axiom.co/docs/reference/edge-deployments). - -## Cloud setup - -1. Create a dataset for traces, such as `stagehand-traces`. Axiom wants a dedicated dataset per OpenTelemetry signal. Do not send these spans to a metrics dataset. -2. Create an API token with ingest access to that dataset. Personal access tokens cannot ingest. -3. Set `AXIOM_API_TOKEN`, `AXIOM_DATASET`, and `AXIOM_DOMAIN`. `AXIOM_DOMAIN` is the ingest host from Settings > General. US East 1 is `us-east-1.aws.edge.axiom.co`. EU Central 1 is `eu-central-1.aws.edge.axiom.co`. - -## Configure Stagehand - - - -```typescript -import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; - -const browser = await localBrowser.launch(); -const stagehand = await Stagehand.create({ - browser, - telemetry: { - traces: { - endpoint: `https://${process.env.AXIOM_DOMAIN ?? "us-east-1.aws.edge.axiom.co"}/v1/traces`, - headers: { - Authorization: `Bearer ${process.env.AXIOM_API_TOKEN}`, - "X-Axiom-Dataset": process.env.AXIOM_DATASET!, - }, - }, - }, -}); - -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": f"https://{os.environ.get('AXIOM_DOMAIN', 'us-east-1.aws.edge.axiom.co')}/v1/traces", - "headers": { - "Authorization": f"Bearer {os.environ['AXIOM_API_TOKEN']}", - "X-Axiom-Dataset": os.environ["AXIOM_DATASET"], - }, - } - }), -) - -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: "https://" + envOr("AXIOM_DOMAIN", "us-east-1.aws.edge.axiom.co") + "/v1/traces", - Headers: stagehand.TelemetryTracesHeaders{ - "Authorization": "Bearer " + os.Getenv("AXIOM_API_TOKEN"), - "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET"), - }, - }, - }, - }) - 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 dataset and go to Stream. Filter on `['service.name'] == "stagehand-service-worker"`. - -You should see `operation` spans for `observe` (and `act` / `extract` when you call those), plus a `log` span for each log record. The type is on `stagehand.span.type`. Operation payloads are JSON on `stagehand.span.data`. - -A starting query. Replace the dataset name if yours is different. - -```apl -['stagehand-traces'] -| where ['service.name'] == "stagehand-service-worker" -| where ['stagehand.span.type'] == "operation" -``` - -If Stream is empty, check the host first. `api.axiom.co` is the management API. OTLP ingest uses the edge domain, and a mismatched region returns 400. Then confirm the token can ingest to this dataset and that you called `close()`. diff --git a/packages/docs/v4/observability/honeycomb.mdx b/packages/docs/v4/observability/honeycomb.mdx deleted file mode 100644 index e86c8ea590..0000000000 --- a/packages/docs/v4/observability/honeycomb.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: "Honeycomb" -sidebarTitle: "Honeycomb" -description: "Query Stagehand browser traces in Honeycomb." ---- - -Honeycomb is built for asking questions of high-cardinality span attributes. You do not pick dimensions in advance. Filter on `service.name`, break down on `stagehand.span.type`, and heat-map duration when an `observe` call is slow. - -See [Honeycomb's OpenTelemetry documentation](https://docs.honeycomb.io/send-data/opentelemetry/). - -## Cloud setup - -1. Create an environment or select an existing one. Current Honeycomb routes by environment, not by a dataset header. `x-honeycomb-dataset` is only for Honeycomb Classic. -2. Create an ingest API key and set `HONEYCOMB_API_KEY`. -3. US accounts use `https://api.honeycomb.io/v1/traces`. EU accounts use `https://api.eu1.honeycomb.io/v1/traces`. Set `HONEYCOMB_OTLP_TRACES_ENDPOINT` when you are on EU. - -## 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.HONEYCOMB_OTLP_TRACES_ENDPOINT ?? "https://api.honeycomb.io/v1/traces", - headers: { - "x-honeycomb-team": process.env.HONEYCOMB_API_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.get("HONEYCOMB_OTLP_TRACES_ENDPOINT", "https://api.honeycomb.io/v1/traces"), - "headers": { - "x-honeycomb-team": os.environ["HONEYCOMB_API_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 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("HONEYCOMB_OTLP_TRACES_ENDPOINT", "https://api.honeycomb.io/v1/traces"), - Headers: stagehand.TelemetryTracesHeaders{ - "x-honeycomb-team": os.Getenv("HONEYCOMB_API_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 Query in the environment that owns the ingest key. Filter `service.name = stagehand-service-worker`. - -Honeycomb indexes the attributes on each span, so you can break down on `stagehand.span.type` without creating a schema. Operation spans wrap `act`, `observe`, and `extract`. Log spans are the individual log records, with the text on `stagehand.log.message`. - -A first query: - -``` -service.name = stagehand-service-worker -AND stagehand.span.type = operation -``` - -Group by `name` and look at `HEATMAP(duration_ms)` when an observe call is slow. BubbleUp is useful once you have more than a handful of runs. - -A US key against `api.eu1.honeycomb.io` is rejected. If Query is empty after a short script, `close()` probably never flushed the batch. diff --git a/packages/docs/v4/observability/hyperdx.mdx b/packages/docs/v4/observability/hyperdx.mdx deleted file mode 100644 index 75c74f7e77..0000000000 --- a/packages/docs/v4/observability/hyperdx.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: "HyperDX" -sidebarTitle: "HyperDX" -description: "Search Stagehand traces and logs in HyperDX." ---- - -HyperDX is a ClickHouse-backed search UI for traces, logs, and metrics in one place. You jump from a slow span to the log line next to it without stitching timestamps by hand. Send logs to the same instance if you already tail them there. - -See [HyperDX's OpenTelemetry documentation](https://www.hyperdx.io/docs/install/opentelemetry). The HTTP ingest host is `https://in-otel.hyperdx.io`. Auth is the raw ingestion key in `Authorization`. Do not prefix `Bearer`. - -## Cloud setup - -1. Copy the ingestion key from Team Settings. -2. Set `HYPERDX_API_KEY`. - -## Configure Stagehand - - - -```typescript -import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; - -const browser = await localBrowser.launch(); -const stagehand = await Stagehand.create({ - browser, - telemetry: { - traces: { - endpoint: "https://in-otel.hyperdx.io/v1/traces", - headers: { - Authorization: process.env.HYPERDX_API_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": "https://in-otel.hyperdx.io/v1/traces", - "headers": { - "Authorization": os.environ["HYPERDX_API_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: "https://in-otel.hyperdx.io/v1/traces", - Headers: stagehand.TelemetryTracesHeaders{ - "Authorization": os.Getenv("HYPERDX_API_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. - -## HyperDX Local - -Run HyperDX Local when you want the same search UI on your machine. The UI is `http://localhost:8080`. OTLP/HTTP is `http://localhost:4318`. The local receiver does not validate the key. HyperDX-maintained SDKs still want a non-empty value. Stagehand does not, so empty headers are fine. - -```bash -docker run -p 8000:8000 -p 4318:4318 -p 4317:4317 -p 8080:8080 -p 8002:8002 docker.hyperdx.io/hyperdx/hyperdx-local -``` - - - -```typescript -const stagehand = await Stagehand.create({ - browser, - telemetry: { - traces: { - endpoint: "http://localhost:4318/v1/traces", - headers: {}, - }, - }, -}); -``` - - -```python -stagehand = await Stagehand.create( - browser=browser, - telemetry=TelemetryConfig.model_validate({ - "traces": {"endpoint": "http://localhost:4318/v1/traces", "headers": {}}, - }), -) -``` - - -```go -client, err := stagehand.Create(ctx, stagehand.CreateOptions{ - Browser: browser, - Telemetry: stagehand.TelemetryConfig{ - Traces: stagehand.TelemetryTraces{ - Endpoint: "http://localhost:4318/v1/traces", - }, - }, -}) -``` - - - -## Verify traces - -Open search and select the trace source. Filter for `stagehand-service-worker`. - -You should see an `operation` span for `observe`, with `log` spans under the same trace. HyperDX's value is clicking from that operation into any log line that landed at the same time, so send logs to the same instance if you have them. - -On Local, use `http://localhost:8080`. If search is empty, confirm the container mapped 4318 and that the process called `close()`. A `Bearer ` prefix on the cloud `Authorization` header is a common 401. diff --git a/packages/docs/v4/observability/laminar.mdx b/packages/docs/v4/observability/laminar.mdx deleted file mode 100644 index 2910583fc5..0000000000 --- a/packages/docs/v4/observability/laminar.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: "Laminar" -sidebarTitle: "Laminar" -description: "Send Stagehand browser traces to Laminar." ---- - -Laminar is an OpenTelemetry-native observability product for agents. It turns spans into a transcript you can read, then lets you run SQL over those traces and attach signals. That is a different job from a generic APM. - -Stagehand v4 talks to Laminar over OTLP. You get the `act` / `observe` / `extract` tree and the log spans. Laminar also publishes a native SDK wrapper for Stagehand v2 and v3 that records the browser session and LLM calls. That wrapper is a different integration than this page. - -See [Laminar's OpenTelemetry troubleshooting](https://laminar.sh/docs/tracing/troubleshooting-opentelemetry). Laminar prefers gRPC. Stagehand exports OTLP/HTTP, so use the HTTP traces URL on port 443. - -## Cloud setup - -1. Create a project API key in Laminar. -2. Set `LMNR_PROJECT_API_KEY`. - -## Configure Stagehand - - - -```typescript -import { localBrowser, Stagehand } from "@browserbasehq/stagehand"; - -const browser = await localBrowser.launch(); -const stagehand = await Stagehand.create({ - browser, - telemetry: { - traces: { - endpoint: "https://api.lmnr.ai:443/v1/traces", - headers: { - Authorization: `Bearer ${process.env.LMNR_PROJECT_API_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": "https://api.lmnr.ai:443/v1/traces", - "headers": { - "Authorization": f"Bearer {os.environ['LMNR_PROJECT_API_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: "https://api.lmnr.ai:443/v1/traces", - Headers: stagehand.TelemetryTracesHeaders{ - "Authorization": "Bearer " + os.Getenv("LMNR_PROJECT_API_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 the Laminar project and select Traces. Filter for `stagehand-service-worker`. You should see an `operation` span for `observe`, with `log` spans on the same trace. Laminar will try to render that tree as a transcript. Stagehand's OTLP payload is operation and log attributes, not `gen_ai.*` conventions, so expect a span tree first and a reconstructed chat second. - -Laminar documents that its HTTP endpoint expects protobuf, not JSON. Stagehand's runtime exporter sends OTLP/HTTP JSON. If traces never appear or the exporter logs a 500, send them through an [OpenTelemetry Collector](/v4/observability/otel-collector) that forwards protobuf to `https://api.lmnr.ai:443`. A 404 usually means the path is missing `/v1/traces`. Hitting the gRPC port with HTTP produces a parse error. - -The native v2/v3 wrapper is the path that also captures the session recording. This OTLP setup does not. diff --git a/packages/docs/v4/observability/otel-collector.mdx b/packages/docs/v4/observability/otel-collector.mdx index d9435f4302..d5f5b42610 100644 --- a/packages/docs/v4/observability/otel-collector.mdx +++ b/packages/docs/v4/observability/otel-collector.mdx @@ -6,7 +6,7 @@ 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. Laminar's HTTP endpoint is the case that comes up most. It expects protobuf. The collector can receive Stagehand's JSON and export protobuf. +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 @@ -61,7 +61,7 @@ service: exporters: [otlphttp/first, otlphttp/second] ``` -Some providers use a header other than `Authorization`. Honeycomb wants `x-honeycomb-team`. LangSmith wants `x-api-key` and `Langsmith-Project`. Sentry wants `x-sentry-auth`. Put those on the matching exporter. +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 diff --git a/packages/docs/v4/observability/overview.mdx b/packages/docs/v4/observability/overview.mdx index 390a2c8789..b8083e40d0 100644 --- a/packages/docs/v4/observability/overview.mdx +++ b/packages/docs/v4/observability/overview.mdx @@ -23,21 +23,9 @@ Configure the endpoint in [`Stagehand.create()`](/v4/configuration/observability These backends all speak OTLP. They do not all do the same job with the spans once they arrive. - - APL queries over Stagehand spans in a dataset, next to your logs. - Score browser runs in the same project as your LLM evals. - - High-cardinality queries over span attributes, no schema first. - - - Search traces and logs in one ClickHouse-backed UI. - - - Agent transcripts and SQL over OTLP spans from browser runs. - LLM tracing with sessions, scores, and a v4 observation table. @@ -47,11 +35,7 @@ These backends all speak OTLP. They do not all do the same job with the spans on Tie Stagehand failures to the same project as your application errors. - - Open-source APM. Traces, metrics, and logs in one regional endpoint. - - Fan out one Stagehand export to more than one backend, or transcode JSON OTLP to protobuf. + Fan out one Stagehand export to more than one backend, or transcode JSON OTLP to protobuf. - diff --git a/packages/docs/v4/observability/signoz.mdx b/packages/docs/v4/observability/signoz.mdx deleted file mode 100644 index 322b76cdc3..0000000000 --- a/packages/docs/v4/observability/signoz.mdx +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: "SigNoz" -sidebarTitle: "SigNoz" -description: "Send Stagehand browser traces to SigNoz." ---- - -SigNoz is an OpenTelemetry-first APM. Traces, metrics, and logs share one backend. The trace view is a service map plus a span waterfall, which is what you want when Stagehand is one more service next to the ones you already watch there. - -Cloud ingest is regional. Self-hosted exposes the usual collector port `4318` and does not need an ingestion key. See [SigNoz Cloud ingestion](https://signoz.io/docs/ingestion/signoz-cloud/overview/). - -## Cloud setup - -1. Open Settings > Ingestion and copy the regional traces endpoint and ingestion key. -2. Set `SIGNOZ_OTLP_TRACES_ENDPOINT` and `SIGNOZ_INGESTION_KEY`. The traces URL looks like `https://ingest..signoz.cloud:443/v1/traces`. Cloud uses port 443 for both HTTP and gRPC, not 4318. - -## 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.SIGNOZ_OTLP_TRACES_ENDPOINT!, - headers: { - "signoz-ingestion-key": process.env.SIGNOZ_INGESTION_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["SIGNOZ_OTLP_TRACES_ENDPOINT"], - "headers": { - "signoz-ingestion-key": os.environ["SIGNOZ_INGESTION_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("SIGNOZ_OTLP_TRACES_ENDPOINT"), - Headers: stagehand.TelemetryTracesHeaders{ - "signoz-ingestion-key": os.Getenv("SIGNOZ_INGESTION_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. - -`signoz-access-token` and `Authorization: Bearer ` are deprecated on Cloud. Use `signoz-ingestion-key`. - -## Self-hosted SigNoz - -Point Stagehand at the OTLP/HTTP receiver on your SigNoz collector. The default local installation does not require an ingestion header. - - - -```typescript -const stagehand = await Stagehand.create({ - browser, - telemetry: { - traces: { - endpoint: "http://localhost:4318/v1/traces", - headers: {}, - }, - }, -}); -``` - - -```python -stagehand = await Stagehand.create( - browser=browser, - telemetry=TelemetryConfig.model_validate({ - "traces": {"endpoint": "http://localhost:4318/v1/traces", "headers": {}}, - }), -) -``` - - -```go -client, err := stagehand.Create(ctx, stagehand.CreateOptions{ - Browser: browser, - Telemetry: stagehand.TelemetryConfig{ - Traces: stagehand.TelemetryTraces{ - Endpoint: "http://localhost:4318/v1/traces", - }, - }, -}) -``` - - - -## Verify traces - -Open Traces and filter for `service.name = stagehand-service-worker`. Open a trace. You should see an `operation` span for `observe`, with `log` spans underneath. - -SigNoz is useful once Stagehand is one service among others. The waterfall shows how long the browser step sat relative to the handler that called it. For a lone local script, the same view is a short tree with one operation and some logs, which is enough to confirm ingest. - -If Cloud shows nothing, the usual miss is the region in the host (`ingest..signoz.cloud`) or sending to 4318 instead of 443. Payloads over 16 MB are rejected. Then confirm `close()` ran.