DevTools
DevTools installs a tracer that mirrors the current tracer and streams its
telemetry to an external devtools process: span starts, span events, span
completions, and on-demand metric snapshots. The transport is a Socket —
typically a WebSocket — defaulting to ws://localhost:34437.
The mental model is important: DevTools only installs the client side. The
devtools server (the UI / collector you connect to) must already be running and
reachable at the configured URL. The client connects, sends periodic Ping
heartbeats, forwards every span as it is created/updated/ended, and replies with
a MetricsSnapshot whenever the server sends a MetricsRequest.
Because it wraps the current tracer rather than replacing it, DevTools composes with your normal tracing setup — see Tracing for spans and Metrics for the metric types that are snapshotted.
Common case
Section titled “Common case”Import DevTools from effect/unstable/devtools and provide
DevTools.layer() to your runtime. Every span you create with Effect.withSpan
(and every span its children create) is forwarded to the devtools process.
import { Effect } from "effect"import { DevTools } from "effect/unstable/devtools"
const program = Effect.gen(function* () { yield* Effect.log("doing work") yield* Effect.sleep("100 millis")}).pipe(Effect.withSpan("work"))
// `DevTools.layer()` uses the global WebSocket and connects to// ws://localhost:34437 by default. The span tree shows up in the// connected devtools UI.program.pipe( Effect.provide(DevTools.layer()), Effect.runFork)DevTools.layer() uses globalThis.WebSocket, so it requires nothing else and
returns a Layer<never>. Point it at a different endpoint by passing a URL:
import { DevTools } from "effect/unstable/devtools"
const DevToolsLive = DevTools.layer("ws://localhost:9000")Choosing a layer
Section titled “Choosing a layer”DevTools ships three layers that differ only in how the underlying Socket is
obtained.
import { Layer } from "effect"import { DevTools } from "effect/unstable/devtools"import { Socket } from "effect/unstable/socket"
// 1. Global WebSocket — no requirements (most apps).const a: Layer.Layer<never> = DevTools.layer()
// 2. WebSocket, but you provide the WebSocketConstructor// (e.g. a Node `ws` polyfill in a non-browser runtime).const b: Layer.Layer<never, never, Socket.WebSocketConstructor> = DevTools.layerWebSocket("ws://localhost:34437")
// 3. Install the tracer over a Socket you already have.const c: Layer.Layer<never, never, Socket.Socket> = DevTools.layerSocketWhen the runtime has no global WebSocket, use layerWebSocket and supply a
constructor (Effect ships Socket.layerWebSocketConstructorGlobal, or you can
provide your own):
import { Layer } from "effect"import { DevTools } from "effect/unstable/devtools"import { Socket } from "effect/unstable/socket"
const DevToolsLive = DevTools.layerWebSocket("ws://localhost:34437").pipe( Layer.provide(Socket.layerWebSocketConstructorGlobal))DevTools reference
Section titled “DevTools reference”The application-facing entry point. Import the DevTools namespace from
effect/unstable/devtools.
DevTools.layer
Section titled “DevTools.layer”layer(url = "ws://localhost:34437"): Layer<never> — the common entry point.
Installs the devtools tracer over a WebSocket using the global WebSocket
constructor, so the resulting layer has no remaining requirements.
import { DevTools } from "effect/unstable/devtools"
DevTools.layer() // => Layer<never> (connects to ws://localhost:34437)DevTools.layer("ws://localhost:9000") // => Layer<never>DevTools.layerWebSocket
Section titled “DevTools.layerWebSocket”layerWebSocket(url = "ws://localhost:34437"): Layer<never, never, Socket.WebSocketConstructor>
— like layer, but leaves WebSocketConstructor as an unfilled requirement so
you decide which WebSocket implementation to use.
import { Layer } from "effect"import { DevTools } from "effect/unstable/devtools"import { Socket } from "effect/unstable/socket"
DevTools.layerWebSocket("ws://localhost:34437").pipe( Layer.provide(Socket.layerWebSocketConstructorGlobal)) // => Layer<never>DevTools.layerSocket
Section titled “DevTools.layerSocket”layerSocket: Layer<never, never, Socket.Socket> — installs the devtools tracer
over an existing Socket transport (it is an alias for
DevToolsClient.layerTracer). Use this when an integration already produced a
Socket.
import { Layer } from "effect"import { DevTools } from "effect/unstable/devtools"
// Provide your own Socket layer (e.g. a TCP socket) and the tracer// streams telemetry over it.declare const SocketLive: Layer.Layer< import("effect/unstable/socket").Socket.Socket>const DevToolsLive = DevTools.layerSocket.pipe(Layer.provide(SocketLive))DevToolsClient reference (advanced)
Section titled “DevToolsClient reference (advanced)”Low-level client and tracer wiring over a Socket. Import the DevToolsClient
namespace from effect/unstable/devtools. Most readers only need the DevTools
layers above; reach for this module to build custom transports or to send
telemetry by hand.
DevToolsClient (service)
Section titled “DevToolsClient (service)”A Context.Service tagged "effect/devtools/DevToolsClient" whose only member
is sendUnsafe(span | spanEvent) — it enqueues a Span or SpanEvent onto the
socket stream with no back pressure. It is meant for the tracer hot path.
import { Effect } from "effect"import { DevToolsClient } from "effect/unstable/devtools"
const sendEvent = Effect.gen(function* () { const client = yield* DevToolsClient client.sendUnsafe({ _tag: "SpanEvent", traceId: "trace-1", spanId: "span-1", name: "checkpoint", startTime: 0n, attributes: { stage: "init" } }) // => void (enqueued, fire-and-forget)})DevToolsClient.make
Section titled “DevToolsClient.make”make: Effect<DevToolsClient, never, Scope | Socket.Socket> — builds the client
service over the current Socket. It is scoped because it forks background
fibers for the socket stream and the 3-second ping heartbeat, and registers a
finalizer that flushes a final metrics snapshot on shutdown.
import { Effect } from "effect"import { DevToolsClient } from "effect/unstable/devtools"
const program = Effect.gen(function* () { const client = yield* DevToolsClient.make // requires Scope + Socket // ...use client.sendUnsafe(...)})DevToolsClient.layer
Section titled “DevToolsClient.layer”layer: Layer<DevToolsClient, never, Socket.Socket> — provides the
DevToolsClient service from the current Socket via make. It only provides
the client; it does not install the devtools tracer.
import { DevToolsClient } from "effect/unstable/devtools"
DevToolsClient.layer // => Layer<DevToolsClient, never, Socket.Socket>DevToolsClient.makeTracer
Section titled “DevToolsClient.makeTracer”makeTracer: Effect<Tracer.Tracer, never, DevToolsClient> — creates a Tracer
that delegates to the current tracer while sending each span’s start, events, and
end to the DevToolsClient. This is the wrapping tracer that powers the higher
layers.
import { Effect, Layer, Tracer } from "effect"import { DevToolsClient } from "effect/unstable/devtools"
// Build the forwarding tracer and install it as the runtime tracer.const TracerLive = Layer.effect(Tracer.Tracer, DevToolsClient.makeTracer)// => Layer<never, never, DevToolsClient>DevToolsClient.layerTracer
Section titled “DevToolsClient.layerTracer”layerTracer: Layer<never, never, Socket.Socket> — combines layer and
makeTracer: it creates the client from the current Socket and installs the
forwarding tracer. DevTools.layerSocket is exactly this layer.
import { DevToolsClient } from "effect/unstable/devtools"
DevToolsClient.layerTracer // => Layer<never, never, Socket.Socket>DevToolsServer reference (building a server)
Section titled “DevToolsServer reference (building a server)”Runs the server side of the protocol — useful if you are building the
collector that devtools clients connect to. Import from
effect/unstable/devtools. It decodes NDJSON messages per
connection, answers Ping with Pong internally, and hands every other request
to your handler. It does not interpret spans or metrics itself.
DevToolsServer.Client (interface)
Section titled “DevToolsServer.Client (interface)”The per-connection handle passed to your handler:
import type { Queue, Effect } from "effect"import type { DevToolsSchema } from "effect/unstable/devtools"
interface Client { // Non-ping requests received from this socket: Span | SpanEvent | MetricsSnapshot readonly queue: Queue.Dequeue<DevToolsSchema.Request.WithoutPing> // Send a non-pong response back to the client: MetricsRequest readonly send: (_: DevToolsSchema.Response.WithoutPong) => Effect.Effect<void>}The queue is scoped to the socket-processing fiber and is shut down when that fiber terminates — treat it as per-connection state, not a process-wide bus.
DevToolsServer.run
Section titled “DevToolsServer.run”run(handle): Effect<never, SocketServerError, R | SocketServer> — runs the
socket server, requiring a SocketServer. Each accepted socket becomes one
Client; Ping is answered with Pong automatically, and all other requests
are offered to client.queue.
import { Effect, Queue } from "effect"import { DevToolsServer } from "effect/unstable/devtools"
// A minimal server: drain incoming telemetry and periodically request metrics.const server = DevToolsServer.run((client) => Effect.gen(function* () { // ask the client for a metrics snapshot yield* client.send({ _tag: "MetricsRequest" })
// process everything the client sends (spans, span events, snapshots) yield* Queue.take(client.queue).pipe( Effect.flatMap((request) => Effect.log("devtools request", request._tag)), Effect.forever ) }))// server: Effect<never, SocketServerError, SocketServer>// provide a SocketServer layer (e.g. a WebSocket server) to run it.DevToolsSchema reference (wire types)
Section titled “DevToolsSchema reference (wire types)”The DevToolsSchema namespace (from effect/unstable/devtools) defines the serialized message shapes
exchanged over the socket. These describe transport payloads, not the
in-memory tracer/metric structures: timestamps are bigints, ended-span exits
drop their success value via Exit.asVoid, and attributes are intentionally
open-ended. You only touch these schemas when implementing a custom transport;
the table below is an index — read the source for the full field definitions.
| Schema | Direction | Description |
|---|---|---|
Span | client → server | Span telemetry: id, trace id, name, sampled flag, attributes, status, optional parent. |
ExternalSpan | embedded | Parent context for a span created outside the current span tree (spanId, traceId, sampled). |
ParentSpan | embedded | A span’s parent — union of Span and ExternalSpan. |
SpanEvent | client → server | A named event on a span (traceId, spanId, name, startTime, optional attributes). |
SpanStatusStarted | embedded | Span status with startTime only (not yet ended). |
SpanStatusEnded | embedded | Span status with startTime, endTime, and encoded exit. |
SpanStatus | embedded | Union of SpanStatusStarted and SpanStatusEnded. |
Ping | client → server | Heartbeat sent by the client every 3 seconds. |
Pong | server → client | Heartbeat reply (sent automatically by DevToolsServer). |
MetricsRequest | server → client | Asks the client to send its current metrics. |
MetricsSnapshot | client → server | Carries an array of Metric snapshots. |
MetricLabel | embedded | A metric label { key, value } pair. |
Metric | embedded | Union of Counter, Frequency, Gauge, Histogram, Summary. |
Counter | embedded | { count, incremental }. |
Frequency | embedded | { occurrences: ReadonlyMap<string, number> }. |
Gauge | embedded | { value }. |
Histogram | embedded | { buckets, count, min, max, sum }. |
Summary | embedded | { quantiles, count, min, max, sum }. |
Request | union | Messages from client → server: Ping | Span | SpanEvent | MetricsSnapshot. |
Response | union | Messages from server → client: Pong | MetricsRequest. |
Request.WithoutPing | type | Request excluding Ping — what DevToolsServer handlers see. |
Response.WithoutPong | type | Response excluding Pong — what handlers may send. |
import { Schema } from "effect"import { DevToolsSchema } from "effect/unstable/devtools"
// Decode an incoming wire message in a custom transport.const decodeRequest = Schema.decodeUnknownSync(DevToolsSchema.Request)decodeRequest({ _tag: "Ping" }) // => { _tag: "Ping" }