Skip to content

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.

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();

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.

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.

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.

Install the application’s tracing dependencies:

Terminal window
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
observability-otel.ts
import { 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();
}
}
Terminal window
DAYTONA_API_KEY=your-key node observability-otel.ts
# Or: DAYTONA_API_KEY=your-key bun observability-otel.ts

The 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.

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.

Terminal window
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 @sentry/node@11.1.0
observability-sentry-node.ts
import * 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);
}
Terminal window
SENTRY_DSN=your-dsn DAYTONA_API_KEY=your-key node observability-sentry-node.ts

Look 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.

Run a Datadog Agent configured for APM. Keep DD_SERVICE, DD_ENV, sampling and Agent settings in your application’s environment. Install:

Terminal window
npm install beachside @beachside/daytona @opentelemetry/api@1.9.1 dd-trace@6.17.0
observability-datadog-node.ts
import 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();
}
Terminal window
DAYTONA_API_KEY=your-key DD_TRACE_AGENT_URL=http://127.0.0.1:8126 node observability-datadog-node.ts

The 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.

Enable your Agent’s OTLP HTTP receiver or use a trusted Collector that forwards to Datadog. Install:

Terminal window
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.0
observability-datadog-otlp.ts
import { 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();
}
}
Terminal window
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.

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:

observability-sentry-capture.ts
/* 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 Bun
import { 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.

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.

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 are success, error and cancelled.
  • beachside.effect: what is known about changes made by the operation? Values are none, possible, applied, partial and unknown.

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.

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.

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.