Files
o11y/sdk/js/README.md
zeekay d81d32e6c0 feat(sdk): @hanzo/o11y delivers — the ZAP node transport
The frame was already right; nothing arrived. pkg/zapreceiver is a ZAP node, and
a node discards what it receives until the NodeID exchange completes, so writing
a correct frame onto a bare socket produced an accepted connection and zero
decoded batches.

src/transport.ts is that missing layer, read off luxfi/zap rather than guessed:
length-prefixed frames over TCP (little-endian uint32, node.go writeMessage) and
the handshake from node_codec.go — the ID in bytes [0,60), its length at offset
60, in a 64-byte object at header version 2.

Two details the published TypeScript runtime forces. @zap-proto/zap is wire
format only, so the node/peer layer lives here until it grows one. And
StructView is abstract with protected accessors because it is the base that
generated views extend — a handshake has no generated view, so HandshakeView is
that view.

Verified end to end against a live receiver: two spans sent from Node arrive
decoded with trace and parent linkage, attributes, events and status intact.
Published 0.1.0.
2026-07-26 18:31:49 -07:00

1.6 KiB

@hanzo/o11y

Traces to Hanzo o11y over ZAP. No OpenTelemetry, no OTLP, no gRPC, no protobuf.

import { O11y } from '@hanzo/o11y'

const o11y = new O11y({ appName: 'my-service', host: 'o11y.hanzo.svc' })

const span = o11y.startSpan('handle-request')
span.setAttribute('http.method', 'GET')
const child = o11y.startSpan('db-query', span)
child.end()
span.end()

await o11y.shutdown()

A span here is already the shape that goes on the wire, so export is JSON plus a frame — there is no second span model to translate through.

Wire

src/wire.ts mirrors luxfi/trace.SpanBatch field for field, and the frame matches what hanzo/o11y/pkg/zapreceiver decodes: a luxfi/zap envelope at header version 2, message type in the flags high byte (MSG_SPAN_BATCH << 8), JSON in the root object's first field. Verified byte-identical to the Go emitter's FinishWithFlags(MsgSpanBatch << 8).

Adding a field means adding it to all three — this SDK, luxfi/trace, and pkg/zapreceiver — in one change.

Transport

The collector is a ZAP node, not a socket that takes loose frames — until the NodeID exchange completes it discards what arrives. src/transport.ts is that layer: length-prefixed frames over TCP (little-endian uint32), opened by the handshake luxfi/zap defines in node_codec.go — the ID in bytes [0,60) and its length at offset 60.

@zap-proto/zap supplies the wire format and stops there, so the node/peer layer lives here. If it grows one, this file is what to delete.

Verified end to end against a live pkg/zapreceiver: spans sent from Node arrive decoded, with trace/parent linkage, attributes, events and status intact.