Tracing and safe diagnostics
Beachside uses OpenTelemetry to record spans: timed records of SDK calls and their outcomes. When you call the SDK inside an active request or job span, its spans appear as children of that span. Your application sets up tracing, chooses which traces to sample, exports them and shuts tracing down. Importing Beachside does not set up telemetry.
Already using telemetry
Section titled “Already using telemetry”OpenTelemetry
Section titled “OpenTelemetry”Keep your existing OpenTelemetry setup. Beachside uses its provider and active context without an extra tracing option. Initialize telemetry before application work, then call the SDK inside your request or job span:
import { Beachside } from "beachside";import { daytonaAdapter } from "@beachside/daytona";
const client = await Beachside.connect({ adapter: daytonaAdapter, credentials: { apiKey: process.env.DAYTONA_API_KEY! }, config: { target: process.env.DAYTONA_TARGET ?? "us" },});// Use client inside your existing active request/job span.await client.close();Sentry
Section titled “Sentry”If Sentry manages your application’s tracing, add enableOpenTelemetrySetup: true to your existing Sentry.init() configuration. Keep your current DSN, sampling, integrations, error hooks and shutdown. This enables Sentry’s OpenTelemetry bridge, so Beachside spans join Sentry’s active trace without a second provider or exporter. This setup is tested with Sentry 11.1.0 on Node and Bun.
If your application already owns an OpenTelemetry provider, keep that provider instead. Add Sentry.openTelemetryIntegration() to your existing Sentry integrations, set enableOpenTelemetrySetup: false, and leave Sentry’s tracesSampleRate/tracesSampler unset. Your OTel sampler remains in control. To send spans to Sentry, add an OTLP exporter using Sentry.getOtlpTracesEndpoint(dsn) to that provider. Sentry’s custom-pipeline guide covers that existing-provider configuration.
Datadog
Section titled “Datadog”For Node applications already running dd-trace, register its OpenTelemetry bridge once during your existing startup, before importing application modules. Keep the existing Datadog initialization, plugins, sampling and Agent configuration:
import tracer from "dd-trace";
// Your application's tracer.init(...) already ran.const provider = new tracer.TracerProvider();provider.register();Beachside spans now join the active Datadog request/job trace. Do not register another OTel SDK provider on this path. The bridge is tested with dd-trace 6.17.0 on Node 22. Datadog’s API bridge instructions describe startup and custom instrumentation.
For Bun, or an application already exporting OTel to Datadog, keep the OTel provider and use an Agent/Collector OTLP endpoint. The complete OTLP example below works on both runtimes.
Starting fresh
Section titled “Starting fresh”A usable Beachside release has not been published yet. Registry versions 0.0.0 are name-reservation placeholders. The installation commands below are for a future release; for now, use the repository setup and example commands with workspace packages.
Each complete example creates and destroys one Daytona sandbox inside a beachside-demo application span. It uses the prepared image daytona-small and a 15-minute sandbox lifetime. Set DAYTONA_API_KEY and optionally DAYTONA_TARGET (defaults to us). Running these examples with real credentials allocates a provider sandbox. Use Node 22.18 or later, or Bun 1.3.14, and save the displayed file locally before running it.
See a trace in your terminal
Section titled “See a trace in your terminal”Install the application’s tracing dependencies:
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 @opentelemetry/sdk-trace-node@2.11.0 @opentelemetry/sdk-trace-base@2.11.0 @opentelemetry/context-async-hooks@2.11.0import { trace } from "@opentelemetry/api";import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";import { Beachside, Image } from "beachside";import { daytona } from "@beachside/daytona";
const manager = new AsyncLocalStorageContextManager().enable();
const provider = new NodeTracerProvider({ spanProcessors: [new BatchSpanProcessor(new ConsoleSpanExporter())],});
provider.register({ contextManager: manager });
try { const client = await Beachside.connect( daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: process.env.DAYTONA_TARGET ?? "us", ttlMinutes: 15, network: "daytona-default", }), );
try { await trace.getTracer("my-application").startActiveSpan("beachside-demo", async (span) => { try { const box = await client.sandboxes.create({ environment: Image.prepared("daytona-small"), });
await box.destroy(); } finally { span.end(); } }); } finally { await client.close(); }} finally { try { await provider.shutdown(); } finally { manager.disable(); }}DAYTONA_API_KEY=your-key node observability-otel.ts# Or: DAYTONA_API_KEY=your-key bun observability-otel.tsThe console exporter prints the beachside-demo parent and beachside.sandbox.create, beachside.prepare, beachside.submit, beachside.wait, destroy and close spans. No collector or vendor account is needed for console export.
Sentry
Section titled “Sentry”Install beachside, @beachside/daytona, @opentelemetry/api@1.9.1 and the runtime’s Sentry package. Set SENTRY_DSN as well as your Daytona key. These examples keep Sentry’s default integrations and error reporting. They enable its OTel bridge and export SDK spans through Sentry’s existing tracing pipeline.
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 @sentry/node@11.1.0import * as Sentry from "@sentry/node";import { Beachside, Image } from "beachside";import { daytona } from "@beachside/daytona";
Sentry.init({ dsn: process.env.SENTRY_DSN!, tracesSampleRate: 1, enableOpenTelemetrySetup: true,});
try { const client = await Beachside.connect( daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: process.env.DAYTONA_TARGET ?? "us", ttlMinutes: 15, network: "daytona-default", }), );
try { await Sentry.startSpan({ name: "beachside-demo" }, async () => { const box = await client.sandboxes.create({ environment: Image.prepared("daytona-small"), });
await box.destroy(); }); } finally { await client.close(); }} finally { await Sentry.close(2000);}SENTRY_DSN=your-dsn DAYTONA_API_KEY=your-key node observability-sentry-node.tsbun add beachside @beachside/daytona @opentelemetry/api@1.9.1 @sentry/bun@11.1.0import * as Sentry from "@sentry/bun";import { Beachside, Image } from "beachside";import { daytona } from "@beachside/daytona";
Sentry.init({ dsn: process.env.SENTRY_DSN!, tracesSampleRate: 1, enableOpenTelemetrySetup: true,});
try { const client = await Beachside.connect( daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: process.env.DAYTONA_TARGET ?? "us", ttlMinutes: 15, network: "daytona-default", }), );
try { await Sentry.startSpan({ name: "beachside-demo" }, async () => { const box = await client.sandboxes.create({ environment: Image.prepared("daytona-small"), });
await box.destroy(); }); } finally { await client.close(); }} finally { await Sentry.close(2000);}SENTRY_DSN=your-dsn DAYTONA_API_KEY=your-key bun observability-sentry-bun.tsLook for beachside-demo and its SDK child spans in your project’s traces. Local tests verify exported Sentry events and span relationships. They do not verify delivery to your account or how Sentry displays the trace.
Datadog on Node
Section titled “Datadog on Node”Run a Datadog Agent configured for APM. Keep DD_SERVICE, DD_ENV, sampling and Agent settings in your application’s environment. Install:
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 dd-trace@6.17.0import tracer from "dd-trace";
tracer.init({ service: "beachside-demo" });
const provider = new tracer.TracerProvider();
provider.register();
// Import the SDK after registering the application's existing Datadog tracer.const { Beachside, Image } = await import("beachside");
const { daytona } = await import("@beachside/daytona");
const client = await Beachside.connect( daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: process.env.DAYTONA_TARGET ?? "us", ttlMinutes: 15, network: "daytona-default", }),);
try { await tracer.trace("beachside-demo", async () => { const box = await client.sandboxes.create({ environment: Image.prepared("daytona-small"), });
await box.destroy(); });} finally { await client.close();}DAYTONA_API_KEY=your-key DD_TRACE_AGENT_URL=http://127.0.0.1:8126 node observability-datadog-node.tsThe OTel bridge sends beachside-demo and its SDK child spans through dd-trace to the Agent. Local tests check the exported Datadog trace data using a stand-in for the Agent. They do not verify a real Agent or the Datadog UI.
Datadog through OTLP on Node or Bun
Section titled “Datadog through OTLP on Node or Bun”Enable your Agent’s OTLP HTTP receiver or use a trusted Collector that forwards to Datadog. Install:
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 @opentelemetry/sdk-trace-node@2.11.0 @opentelemetry/sdk-trace-base@2.11.0 @opentelemetry/context-async-hooks@2.11.0 @opentelemetry/exporter-trace-otlp-http@0.222.0import { trace } from "@opentelemetry/api";import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";import { Beachside, Image } from "beachside";import { daytona } from "@beachside/daytona";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
const manager = new AsyncLocalStorageContextManager().enable();
const provider = new NodeTracerProvider({ spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? "http://127.0.0.1:4318/v1/traces", timeoutMillis: 1000, }), ), ],});
provider.register({ contextManager: manager });
try { const client = await Beachside.connect( daytona({ apiKey: process.env.DAYTONA_API_KEY!, target: process.env.DAYTONA_TARGET ?? "us", ttlMinutes: 15, network: "daytona-default", }), );
try { await trace.getTracer("my-application").startActiveSpan("beachside-demo", async (span) => { try { const box = await client.sandboxes.create({ environment: Image.prepared("daytona-small"), });
await box.destroy(); } finally { span.end(); } }); } finally { await client.close(); }} finally { try { await provider.shutdown(); } finally { manager.disable(); }}DAYTONA_API_KEY=your-key OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces bun observability-datadog-otlp.ts# The same file also runs with node.This uses your OpenTelemetry sampling and context setup. It does not load dd-trace on Bun. Look for beachside-demo in your configured backend. Local tests verify the OTLP data that leaves the application; they do not verify account ingestion or the vendor UI.
Optional issue capture and privacy
Section titled “Optional issue capture and privacy”Beachside records span status but does not create Sentry issues automatically. To report an SDK failure, use diagnosticContext(error) to collect a small set of safe fields. Avoid sending the original error, its cause or command output, which may contain private data. This example limits its event processor to one explicit capture and keeps your global beforeSend and default integrations:
/* oxlint-disable anti-slop/no-unknown-parameters -- Applications may catch arbitrary values; only diagnosticContext's bounded record reaches Sentry. */import * as Sentry from "@sentry/node"; // use @sentry/bun on Bunimport { context, trace } from "@opentelemetry/api";import { diagnosticContext } from "beachside";
export function captureBeachsideFailure( error: unknown, sentry: Pick<typeof Sentry, "withScope" | "captureMessage"> = Sentry,) { const active = trace.getSpanContext(context.active()); const diagnostic = { ...diagnosticContext(error) };
sentry.withScope((scope) => { scope.addEventProcessor((event) => ({ event_id: event.event_id, timestamp: event.timestamp, level: "error", message: "Beachside operation failed", contexts: { trace: active ? { trace_id: active.traceId, span_id: active.spanId } : undefined, beachside: diagnostic, }, })); sentry.captureMessage("Beachside operation failed", { level: "error" }); });}Call it once inside your request or job’s catch block. The processor builds a new event containing only the allowed diagnostic and trace fields. Unrelated application errors keep their normal messages, exceptions, stacks and tags. Your other event processors and beforeSend still apply. Vendor instrumentation, HTTP instrumentation and your own spans may collect more data than Beachside does; configure their privacy settings separately.
Tracing controls
Section titled “Tracing controls”No option is needed to use the application’s provider. tracing: false disables SDK spans for one client:
import { Beachside } from "beachside";import { daytonaAdapter } from "@beachside/daytona";
const client = await Beachside.connect({ adapter: daytonaAdapter, credentials: { apiKey: process.env.DAYTONA_API_KEY! }, config: { target: "us" }, tracing: false,});await client.close();To supply a provider directly, use tracing: { tracerProvider }. You must still configure a compatible async context manager to connect spans across asynchronous calls. A shared client reads the active context separately for each call. client.close() releases SDK resources; your application must flush and shut down its tracing provider. Disabling SDK tracing does not disable other instrumentation.
Interpret spans and errors
Section titled “Interpret spans and errors”SDK span names are fixed and use OpenTelemetry’s INTERNAL kind. They cover connection, support queries, inventory, sandbox listing, sandbox creation, image builds, execution, files, previews, processes, terminals, inspection, destruction, operation observation/recovery and client close.
The work inside an operation appears as beachside.prepare, beachside.submit and beachside.wait. Explicit advanced observation uses beachside.observe. Convenience calls avoid duplicate spans for their internal public submit/wait methods. Waiting records a poll count and at most eight state-change events; it does not create a span for every unchanged poll.
Two attributes answer different questions:
beachside.call.outcome: did this SDK call succeed, fail or get cancelled? Values aresuccess,errorandcancelled.beachside.effect: what is known about changes made by the operation? Values arenone,possible,applied,partialandunknown.
A successful observation can report that remote work is still pending or its outcome is unknown. A query can successfully report unsupported behavior. A nonzero exec() exit throws NonzeroExitError; its span records the exit code and applied effect because the command ran, even though the SDK call failed.
Cancelling local waiting after submission does not prove that remote work stopped. If a submission response is lost, use observation and recovery to check the original operation instead of submitting it again.
import { diagnosticContext } from "beachside";
try { // await your Beachside operation} catch (error) { const diagnostic = diagnosticContext(error); // Pass this bounded record to your application's issue/reporting system. // Keep the original error/handle if recovery is needed.}diagnosticContext returns an allowed error code and effect, safe Beachside operation/submission IDs when known, the known operation state and recoveryAvailable. State can be pending, completed, rejected, failed or unknown. The helper reads only local data and does no I/O.
recoveryAvailable is true only for immutable references already validated by the SDK. A reference loaded from storage is unverified until recover() returns a handle. Diagnostics do not inspect its opaque token and cannot be used to recover an operation themselves. Keep the original error or handle when recovery is needed.
SDK diagnostics omit invocation keys, native resource identifiers, scope, recovery tokens, URLs and raw error messages or causes. Unknown provider labels become custom. SDK spans omit commands, arguments, environment values, paths, file contents, stdout/stderr, image names, credentials and URLs by default.
Recovery and HTTP instrumentation
Section titled “Recovery and HTTP instrumentation”An operation handle may retain a validated trace context so later spans can link back to the original call. Saved recovery references do not carry that context. After restarting the client, recover inside the current request or job span and use the safe operation ID to correlate work when the original trace is unavailable. Recovery still works when traces are missing or unsampled. A trace link does not guarantee that a vendor displays the work as one continuous tree.
Beachside does not add HTTP transport spans, patch global fetch, or send tracing headers to providers or guest services. Your application’s HTTP instrumentation can add its own spans and headers.
Local tests use real instrumentation-http and verify one CLIENT child span for one adapter file-read request on Node and Bun’s compatible http.request. They do not verify automatic tracing of Bun’s native fetch.
For production HTTP instrumentation, ignore provider and guest destinations by default, send trace headers only to trusted destinations, and remove private URL, query and header data before export. Beachside’s safe fields cannot scrub data collected by other HTTP instrumentation, native SDKs, application spans or vendor captures.
Qualification and debugging
Section titled “Qualification and debugging”From a source checkout, run bun run build:packages, then bun run observability:check. This guide displays the same example files that documentation checks compile and local tracing tests execute. The tests replace provider and vendor endpoints with local stand-ins; they make no live provider calls.
The examples and tests live in packages/sdk-qualification/observability. They cover Node 22.23.3 and Bun 1.3.14 independently, including local export, sanitized issues, sampling, concurrent parent spans and shutdown deadlines. They do not qualify metrics, structured logs, vendor UI or live provider telemetry. Native Datadog tests cover Node; Bun uses OTLP.
If spans are missing, check that tracing is enabled, telemetry was initialized before application work, and Beachside shares the application’s @opentelemetry/api instance. Also check that sampling selected the trace and the exporter finished before its deadline.
If spans are disconnected, check the async context manager and make each call inside the intended request or job span. Supplying a provider does not install a context manager. You can still correlate recovery by operation ID when traces are absent or unsampled.