Observe the SDK
observeSubscribe to every decision and side effect the SDK makes, on node:diagnostics_channel, with redacted frozen payloads, a bounded queue per observer and no cost when nobody listens.
observeFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
observe lets your code, a test or a tracing library watch what the SDK decides and does. It is read-only: an observer can't veto or change a call.
It covers each network, permission and access decision, each AI, MCP, files, pay and share call, and spans for fetches, driver calls and actions. One that throws or stalls never changes a request. Reach for it to feed a metrics tool, to assert in tests, or to debug a refused call.
At a glance
# Observers are code, not configuration. Debug output is an environment variable:
# MAINSDK_DEBUG=observe print every event, with detail fields (dev only)
# MAINSDK_DEBUG=mainsdk:access.*,mainsdk:network.* only matching channels
schema: 1import { observe } from "mainsdk";
const stop = observe("mainsdk:network.*", (e) => {
console.log(e.channel, e.data.host, e.data.decision);
});Drivers
1 drivers. Select one to see its line in main.yaml.
observe:
driver: builtinChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-OBS-0001 | RuntimeError responses and logs | An observer threw or rejected; caught, the request is untouched, and the observer keeps receiving |
MAIN-OBS-0002 | RuntimeError responses and logs | An observer's queue was full and its oldest events were dropped |
MAIN-OBS-0003 | RuntimeError responses and logs | An observer's synchronous work took over 10 ms and blocked the event loop |
MAIN-OBS-0004 | RuntimeError responses and logs | A published payload breaks its channel schema |
MAIN-OBS-0005 | RuntimeError responses and logs | observe() named a channel that isn't in the registry, or a pattern that matches none |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Diagnostics channels with a versioned registry | Phase 1 | 0.3 | observe#registry |
| Bounded observer queues with drop reports | Phase 1 | 0.3 | observe#core |
| Spans for fetch | Phase 1 | 0.3 | observe#tracing |
| Test kit app.events | Phase 1 | 0.3 | observe#testing-events |
Quick start
import { observe } from "mainsdk";
const stop = observe("mainsdk:network.*", (e) => {
console.log(e.channel, e.data.host, e.data.decision);
});
// later: stop()The pattern is an exact channel name or one ending in *. A name that matches no channel throws MAIN-OBS-0005 and names the closest one. The full list is on the channels reference.
Configuration
There is no main.yaml section: observers are code, registered at start. These calls and settings shape them.
observe(pattern, fn, opts?)subscribes and returnsstop.opts.maxbounds the queue (default 1,000).app.observe(pattern, fn, opts?)is the same for one app.observe.channels()lists every channel with its version, kind, publisher and JSON Schema.observe.schema(name)returns the JSON Schema 2020-12 of one payload.observe.flush()waits until every queue is drained.observe.stats()reports delivered and dropped counts.observe.otel(api?)bridges spans to an OpenTelemetry API you pass in, using only the allowed attributes.registerChannel(def)andremoveChannel(name, version)add or remove a channel entry. A changed shape ships as@2beside@1.detailScope(opts, fn)turns debug detail on forfnfor up to 60 minutes. In developmentMAINSDK_DEBUG=observe, or a list of channel patterns, prints matching events with their detail fields. In productionMAINSDK_DEBUGis ignored andMAIN-LOG-0003is logged once.tracedDriver(driver, { publisher, interface, ops })wraps a driver's async operations indriver.callspans.
What you get
Every event has the same envelope: channel, schema, kind, phase, id, time, app, tenant, user, traceId, spanId, parentSpanId, caller, data and detail. The payload is redacted with the log rules, URL query values are removed, and the object is frozen. An observer does not receive events its own calls caused.
Observers run off the request path. When a queue is full the oldest event is dropped and MAIN-OBS-0002 is logged at most once a minute. A throw is caught and logged as MAIN-OBS-0001. Synchronous work over 10 ms is reported as MAIN-OBS-0003.
Errors and checks
The MAIN-OBS-0001 to 0005 codes above are runtime codes: a throwing observer, a full queue, slow synchronous work, a payload that breaks its schema and an unknown channel. See check codes.
Swapping drivers
observe has no drivers. It is part of the core, and the same calls work whatever drivers the app uses. tracedDriver wraps a driver's calls in driver.call spans, and observe.otel() bridges spans to OpenTelemetry.
Testing
createTestApp() gives app.events(pattern?), which returns the recorded events at once, and app.events.clear(). A payload with a field its schema doesn't list throws MAIN-OBS-0004.
In development
With no setup, events from the dev server are written to .mainsdk/events/channels.jsonl, and mainsdk logs --channel "mainsdk:network.*" reads them.
Next
Was this page helpful?