Diagnostics & Dev Mode
Sema Web catches failures rather than letting them take the page down — a component that throws leaves the previous DOM in place, a broken event handler does not break the next one. That is the right default, but it means failures are quiet by default too. This page is how you hear them.
There are two mechanisms, and they compose:
onerror— one hook that receives every caught failure. Use it in production to forward errors to your reporter.dev— a bounded in-memory timeline of what the runtime did. Use it in development to see renders, routes, streams, and script loads alongside the errors.
The error hook
const web = await SemaWeb.create({
onerror(error, context) {
reportToSentry(error, { tags: { context } });
},
});context is a short string naming where the failure came from:
| Context | Meaning |
|---|---|
component:todo-list | the component function threw while rendering |
component-dispose:todo-list | its render effect threw during teardown |
unmount-cleanup:todo-list | an on-mount cleanup threw |
listener-cleanup:click | removing a DOM listener threw |
event-source-cleanup | closing an SSE stream threw |
inline-script:2 | the third inline <script type="text/sema"> failed |
script:/app.sema | an external script failed |
Installing a handler replaces the default (which logs to console.error). It does not turn off the dev timeline: entries are recorded first, then your handler runs, so a production reporter and a local timeline never fight.
Dev mode
const web = await SemaWeb.create({ dev: true });That enables a bounded ring of events, readable through web.diagnostics:
web.diagnostics.all(); // every retained entry, oldest first
web.diagnostics.byKind("error"); // just the failures
web.diagnostics.byKind("route"); // navigation history
web.diagnostics.slowRenders(); // renders at/over the threshold
web.diagnostics.dropped; // how many entries the bound evicted
web.diagnostics.clear(); // start a fresh timelineEach entry is { kind, at, context, detail?, durationMs? }.
What gets recorded
| Kind | Recorded when | detail |
|---|---|---|
error | any failure reaching the error hook | the error message |
render | a component finishes rendering | — (carries durationMs) |
route | the route changes | /todos/42 -> todo-detail, or -> (no match) |
stream | an SSE or LLM stream opens or closes | open / close |
script | a Sema script loads, evaluates, or fails | evaluated, or the error |
Finding slow components
render entries carry a wall-clock duration covering the whole render — the Sema call, SIP construction, and the DOM patch:
const web = await SemaWeb.create({ dev: { slowRenderMs: 8 } });
// ... interact with the app ...
for (const r of web.diagnostics.slowRenders()) {
console.log(r.context, r.durationMs.toFixed(1) + "ms");
}
// component:todo-list 24.3msThe default threshold is 16ms — one frame at 60fps.
Tuning
await SemaWeb.create({
dev: {
limit: 500, // ring size (default: 100)
slowRenderMs: 8, // slow-render threshold (default: 16)
overlay: true, // on-page dev panel (default: false)
},
});The ring is bounded on purpose. A component stuck in a render loop is exactly the bug you want the timeline for, and an unbounded buffer would take the tab down before you could read it. dropped tells you how many entries fell off the back.
The overlay
dev: { overlay: true } mounts a small fixed panel in the bottom-right corner that tails the timeline — colour-coded by kind, newest last, with render durations inline. It is click-through (pointer-events: none), so it never steals input from the app underneath.
The overlay is loaded by a dynamic import(). In the ESM build that makes it a separate chunk your production bundle never fetches; enabling it is the only thing that pulls it in.
Cost when disabled
Dev mode is off by default and costs nothing when off. Entries are built lazily — with dev unset, no entry object is allocated and no detail string is interpolated on the render path. Instrumentation you can leave in the codebase is instrumentation that is still there when you need it.
Teardown
web.dispose() removes the overlay and drops every diagnostics subscriber, so nothing holds a reference to detached DOM. Recorded entries survive disposal — reading the timeline after teardown is usually exactly when you want it.