Diagnostics
When you build modular software, the price is causality — why did this happen? gets harder. The runtime emits a diagnostic event for every interesting thing it does, so you can answer it.
event.emitted — an event was emitted with a given typecommand.dispatched — a command was sentcommand.handler_registered — a command handler was registeredcommand.handled — a command handler ran successfullycommand.failed — a command handler threwcommand.unhandled — no handler for a dispatched commandmodule.installed — a module finished setupruntime.started — every module's start hook has runruntime.stopped — every module's stop hook has runprovider.overwritten — a provider token was assigned twiceboundary.created — a runtime-level boundary handle was createdboundary.destroyed — a runtime-level boundary handle was destroyedThis is not the production observability stream. It’s a development tool — a way to ask the runtime “what just happened?” while you build.
Listening for diagnostics
Section titled “Listening for diagnostics”The runtime exposes a single subscription:
runtime.onDiagnostic((diagnostic) => { console.debug('[kit]', diagnostic.type, diagnostic.detail)})A diagnostic is:
type KitDiagnostic = { type: string timestamp: number detail?: Record<string, unknown>}Subscribe many times; each handler runs.
The debug module
Section titled “The debug module”The @atheory-ai/kitsune-dev package wraps this in a one-line module:
import { debugModule } from '@atheory-ai/kitsune-dev'
shell.modules = [...yourModules, debugModule()]It logs every event and every diagnostic to the console while you build. Remove it for production.
You can pass a custom logger:
debugModule({ log: (msg, detail) => myLogger.debug(msg, detail) })Reading the loop
Section titled “Reading the loop”A typical click in a Quill app produces, in order:
[kit:event] note.saved[kit:diagnostic] event.emitted { eventType: 'note.saved', event: {...} }[kit:diagnostic] command.dispatched { commandType: 'notification.show', command: {...} }[kit:diagnostic] command.handled { commandType: 'notification.show' }If a handler is missing:
[kit:diagnostic] command.unhandled { commandType: 'oops.typo' }If a handler throws:
[kit:diagnostic] command.failed { commandType: 'draft.save', error: TypeError... }Use these to triage why a UI didn’t update. Most “the button didn’t do anything” bugs are visible in the diagnostic stream.
Building a custom diagnostic stream
Section titled “Building a custom diagnostic stream”Diagnostics work with any sink. To record into IndexedDB, ship to a logging service, or feed a future devtools panel:
runtime.onDiagnostic((d) => { diagnosticBuffer.push(d) if (diagnosticBuffer.length > 1000) diagnosticBuffer.shift()})The buffer is a ring of recent diagnostics. A future devtools UI can show this as a timeline.
Diagnostics vs application logging
Section titled “Diagnostics vs application logging”Diagnostics describe the runtime. They are about events, commands, and modules. Application logging — “user X did Y” — should go through events, not diagnostics. The same is true for analytics: emit a session.started event and let an analytics module observe it.
Production posture
Section titled “Production posture”Strip diagnostics in production builds:
- Don’t install
debugModulein production. - Subscribe to
onDiagnosticonly behind a debug flag. - Use a build-time constant if you want to drop the subscription entirely.
The runtime always emits diagnostics, but with no subscribers they’re cheap (a for over an empty set).
Read next
Section titled “Read next”- Reference: Diagnostics Catalog — the full list with payload shapes
- debugModule reference