# DevTools

**Unstable:** These modules live under `effect/unstable/devtools` and the socket / NDJSON wire
protocol may change between releases. The 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:

```ts
const DevToolsLive = DevTools.layer("ws://localhost:9000")
```

<Aside type="note">
The devtools tracer **delegates** to whatever tracer is currently installed. If
you also provide an OpenTelemetry tracing layer, spans flow to both — DevTools
does not take over tracing, it tees off it. The tracer is scoped to the
layer/runtime graph that receives the layer.

## Choosing a layer

`DevTools` ships three layers that differ only in how the underlying `Socket` is
obtained.

```ts
// 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.layerSocket
```

When the runtime has no global `WebSocket`, use `layerWebSocket` and supply a
constructor (Effect ships `Socket.layerWebSocketConstructorGlobal`, or you can
provide your own):

```ts
const DevToolsLive = DevTools.layerWebSocket("ws://localhost:34437").pipe(
  Layer.provide(Socket.layerWebSocketConstructorGlobal)
)
```

---

## `DevTools` reference

The application-facing entry point. Import the `DevTools` namespace from
`effect/unstable/devtools`.

### `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.

```ts
DevTools.layer() // => Layer<never>  (connects to ws://localhost:34437)
DevTools.layer("ws://localhost:9000") // => Layer<never>
```

### `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.

```ts
DevTools.layerWebSocket("ws://localhost:34437").pipe(
  Layer.provide(Socket.layerWebSocketConstructorGlobal)
) // => Layer<never>
```

### `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`.

```ts
// 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)

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)

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.

```ts
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`

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

```ts
const program = Effect.gen(function* () {
  const client = yield* DevToolsClient.make // requires Scope + Socket
  // ...use client.sendUnsafe(...)
})
```

### `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.

```ts
DevToolsClient.layer // => Layer<DevToolsClient, never, Socket.Socket>
```

### `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.

```ts
// 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`

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

```ts
DevToolsClient.layerTracer // => Layer<never, never, Socket.Socket>
```

---

## `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)

The per-connection handle passed to your handler:

```ts
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`

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

```ts
// 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)

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 `bigint`s, 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`. |

```ts
// Decode an incoming wire message in a custom transport.
const decodeRequest = Schema.decodeUnknownSync(DevToolsSchema.Request)
decodeRequest({ _tag: "Ping" }) // => { _tag: "Ping" }
```