Dev Bar
The Via Dev Bar is a built-in, dev-gated debug overlay — like the Symfony Web Debug Toolbar or Laravel Debugbar, but tuned to php-via's signals, scopes, and SSE model. It streams live over the framework's own SSE connection and injects into any page with zero configuration.
It's running on this page right now. Look for the via pill in the bottom-right
corner and click it open — every panel below describes what you're looking at.
Enabling it
The Dev Bar follows devMode by default: on in development, off (and 404-ing all
its endpoints) in production. Override it explicitly with withTracing() — that's
how this public site demos it.
$config = (new Config())
->withDevMode($isDev)
->withTracing(true) // force on even in prod (read-only — see below)
->withTraceBufferSize(100); // optional: ring-buffer depth (default 100)
When tracing is off, Tracer::current() is null and every capture
call site is a no-op — there is no measurable overhead in production.
The six panels
| Panel | Shows |
|---|---|
| Traces | A waterfall of every request, action, and broadcast — nested spans with proportional bars, durations, and attributes. |
| Signals | This context's named signals grouped by scope, with their live values. Editable when writes are enabled. |
| SSE | The wire: every datastar-fetch event, with the real patch payload (elements / signals) expandable inline. |
| Request | Route, context id, and the request span's attributes (method, status, byte size). |
| Scopes | Active scopes, contexts per scope, and connection counts — the php-via-specific view of who shares what. |
| Logs | Server-side log records (level-filtered) plus client errors. See Server logs. |
Adding your own spans
The framework auto-instruments the request → action → render → broadcast path. To time your
own work — a DB query, an HTTP call, an expensive computation — wrap it in
$c->span(). The name's prefix before the first dot becomes its colour category.
$issues = $c->span('db.list_issues', fn () => $repo->all(), ['limit' => 50]);
// Annotate the currently open span from anywhere in the action:
$c->traceAttribute('cache.hit', false);
For a static data layer with no Context in scope, reach the ambient tracer
directly — it participates in whatever trace is open on the current coroutine:
use Mbolli\PhpVia\Tracing\Tracer;
private static function getCell(int $row, int $col): string {
$tracer = Tracer::current();
$run = fn () => /* … the SQLite query … */;
return $tracer === null ? $run() : $tracer->span('db.get_cell', $run, ['row' => $row, 'col' => $col]);
}
The Spreadsheet and
Chat Room examples both emit real
db.* spans this way. Categories map to colours:
| Category | Source | Colour |
|---|---|---|
request | page / action root spans | gray |
render | render.regions, render.component | green |
cache | view-cache hits | yellow |
db | your db.* spans | blue |
sse | broadcast fan-out | violet |
| anything else | your custom spans | teal |
Component renders are recorded as render.component spans tagged with the
component namespace, so you can see exactly which sub-context re-rendered.
Server logs
Anything that goes through the framework logger — $app->log(), the per-context
logger, broker errors, action failures — is teed into the Logs panel and
streamed live. The level filter defaults to info; switch to debug
to see the firehose, or error to focus on failures. Client-side
window.onerror events land here too.
Only logs routed through the framework logger are captured. Raw error_log() or
echo from application code bypass it — route those through
$app->log() if you want them in the panel.
Editing signals
The Signals panel can write values back to the server, the way Livewire / React DevTools let you poke at state. This is gated separately and hard-disabled outside devMode.
$config
->withDevMode(true)
->withTracingWrites(true); // or set VIA_DEVBAR_WRITES=1 isTracingWritesEnabled() begins with a devMode check, so even an
explicit withTracingWrites(true) is ignored when devMode is off —
withTracing(true) on a public site is always read-only. Any visitor who could
reach an editable panel could mutate shared ROUTE/SESSION/GLOBAL
state, so never enable writes on untrusted traffic.
Endpoints & the console
With tracing on, the server exposes these under /_via/*; with tracing off they
all 404, so production never advertises the surface.
| Endpoint | Purpose |
|---|---|
GET /_via | Standalone full-screen Dev Console |
GET /_via/stream | SSE stream multiplexing trace + log events |
GET /_via/scopes | JSON snapshot for the Scopes panel |
POST /_via/signal | Signal write (refused unless writes are enabled) |
GET /_via/devbar.js, .css | Overlay assets |
The overlay is a Shadow-DOM web component, fully isolated from your page's CSS and Datastar
runtime, and it survives full-page morphs. Open /_via
for the console view.
The trace and log buffers live in the worker process, so under worker_num > 1 a
console stream sees one worker's activity. That's fine for a development tool — production runs
with tracing off.
See also
- Development workflow — hot reload and the dev loop
- Deployment — why tracing stays off in production
- Design decisions — the runtime model the traces reflect