MainSDK
Search⌘K
Interfaces

Observability and analytics

Observe the SDK

observe

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

Building for 0.3ships as a beta APIPhase 1 · Core, public betaSpec observeFree in your app, Apache-2.0

Status 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.
Stability levels
Updated 2026-10-28

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: 1

Calls

  • observe(pattern, fn, opts?)
  • observe.channels()
  • observe.schema(name)
  • observe.flush()
  • observe.stats()
  • observe.otel(api?)
  • registerChannel(def)
  • removeChannel(name, version)
  • detailScope(opts, fn)
  • tracedDriver(driver, { publisher, interface, ops })
  • app.observe(pattern, fn, opts?)

Guarantees

Whatever the driver.

  • An observer that throws or stalls never changes a request
  • Payloads are redacted and frozen before any observer sees them
  • A channel with no subscriber costs one property read

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (core (default)
observe:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-OBS-0001RuntimeError responses and logsAn observer threw or rejected; caught, the request is untouched, and the observer keeps receiving
MAIN-OBS-0002RuntimeError responses and logsAn observer's queue was full and its oldest events were dropped
MAIN-OBS-0003RuntimeError responses and logsAn observer's synchronous work took over 10 ms and blocked the event loop
MAIN-OBS-0004RuntimeError responses and logsA published payload breaks its channel schema
MAIN-OBS-0005RuntimeError responses and logsobserve() 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.

FeaturePhaseMilestoneSpec ticket
Diagnostics channels with a versioned registryPhase 10.3observe#registry
Bounded observer queues with drop reportsPhase 10.3observe#core
Spans for fetchPhase 10.3observe#tracing
Test kit app.eventsPhase 10.3observe#testing-events

Quick start

lib/watch.ts
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 returns stop. opts.max bounds 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) and removeChannel(name, version) add or remove a channel entry. A changed shape ships as @2 beside @1.
  • detailScope(opts, fn) turns debug detail on for fn for up to 60 minutes. In development MAINSDK_DEBUG=observe, or a list of channel patterns, prints matching events with their detail fields. In production MAINSDK_DEBUG is ignored and MAIN-LOG-0003 is logged once.
  • tracedDriver(driver, { publisher, interface, ops }) wraps a driver's async operations in driver.call spans.

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?

↑↓ moveEnter openEsc close

Not decided yet