Documentation menu

Dev Bar

The Via Dev Bar is a debug overlay built into php-via, like the Symfony Web Debug Toolbar or Laravel Debugbar, but built around php-via's signals, scopes and SSE stream. It goes into every page without configuration and streams traces and logs as they happen.

It is running on this page. Look for the via pill in the bottom-right corner and click it open. Every panel below describes what you are looking at.

Turning it on

The Dev Bar follows dev mode: on with withDevMode(), off otherwise, and then every /_via endpoint answers 404. withDevBar() overrides that either way: withDevBar(false) turns it off in dev mode, and withDevBar(true) turns it on outside dev mode, read-only, which is how this site shows it.

$config = (new Config())
    ->withDevMode($isDev)
    ->withDevBar(true)                 // also outside dev mode, read-only
    ->withDevBarOptions(traces: 100);  // traces kept per worker (default 100)
Read-only does not mean private. With the Dev Bar on, every visitor sees the traces, logs, scopes, context ids and signal values of every tab on the worker, also other visitors' tabs, as /_via/scopes and the console list them, and the server's request counts and runtime figures in the Stats panel. Session scopes show a hash, never the session cookie, and traces show [redacted] for attributes named like a password, token, cookie or session. Outside development, turn it on only for a demo or for a deployment that only trusted people reach, such as one behind an admin login.

With the Dev Bar off, no trace is recorded and every capture call does nothing. When middleware sets the via.csp_nonce request attribute, the Dev Bar's stylesheet and script carry the nonce, as via_head's tags do (see Content-Security-Policy).

Each tab with the Dev Bar holds two streams, /_sse and /_via/stream. Over HTTP/1.1 a browser opens six connections per host, so with three tabs of one site open, the third tab's actions wait until another tab closes. Over HTTP/2 all streams share one connection: serve the site with withCertificate(), or behind a proxy such as Caddy.

The Dev Bar is separate from the Live Signals panel of php-via's default shell, which shows the page's signals in a sidebar in dev mode only.

The seven panels

PanelShows
TracesA waterfall of every page load, action and broadcast: nested spans with proportional bars, durations and attributes.
SignalsThis page's named signals, grouped by scope, with their live values. Editable when writes are on.
SSEThe wire: every datastar-fetch event, with the patch it carried (elements or signals) expandable inline.
RequestRoute, context id and the request span's attributes (method, status, byte size).
ScopesThe scopes of the worker that answered, named at the top, with the contexts in each and the connection counts: who shares what. With several workers, a tab whose stream is on another worker is not in the list.
StatsEvery figure of getStats()->getAll(), the broadcast tick, and the hook flags, AIO threads and event loop lag of the worker that answered. See Server statistics.
LogsServer log lines, filtered by level, plus errors from the browser. See Server logs.

The ⟲ button in the panel header clears the traces and SSE events it shows. In dev mode it also clears the worker's trace buffer, for everyone who watches that worker; outside dev mode the buffer stays.

Adding your own spans

php-via traces the request, the action, the render and the broadcast by itself. To time your own work, a database query, an HTTP call or an expensive computation, wrap it in $c->span(). The name's prefix before the first dot picks its colour.

$issues = $c->span('db.list_issues', fn () => $repo->all(), ['limit' => 50]);

// Annotate the span that is open, from anywhere in the action:
$c->traceAttribute('cache.hit', false);

A data layer with no Context at hand reaches the tracer directly. It adds to whatever trace is open in the current coroutine, and is null while the Dev Bar is off:

use Mbolli\PhpVia\Tracing\Tracer;

private static function getCell(int $row, int $col): string {
    $tracer = Tracer::current();
    $run = fn (): string => /* the SQLite query */ '';

    return $tracer === null ? $run() : $tracer->span('db.get_cell', $run, ['row' => $row, 'col' => $col]);
}

The Spreadsheet and Chat Room examples record db.* spans this way. Categories map to colours:

CategorySourceColour
requestpage load and action root spansgray
renderrender.regions, render.componentgreen
cacheshared render hitsyellow
dbyour db.* spansblue
ssebroadcast fan-outviolet
anything elseyour own spansteal

A component's render is a render.component span tagged with the component's namespace, so you can see which component rendered again.

Server logs

Everything that goes through php-via's logger, $app->log(), broker errors, failing actions and php-via's own warnings, also goes to the Logs panel as it happens. The level filter starts at info; switch to debug for everything or to error for failures. window.onerror events from the browser land here too.

Only lines that go through php-via's logger show up. error_log() or echo from app code bypass it: send those through $app->log() to see them in the panel.

Editing signals

The Signals panel can write values back to the server, the way Livewire or React DevTools let you poke at state. That needs dev mode and its own switch:

$config
    ->withDevMode(true)
    ->withDevBarOptions(writes: true);   // or set VIA_DEVBAR_WRITES=1
Writes never work outside dev mode. withDevBarOptions(writes: true) has no effect while dev mode is off, so withDevBar(true) on a public site stays read-only. In dev mode, anyone who can reach the page could change ROUTE, SESSION and GLOBAL state shared with other users, so keep writes off on a dev server others can reach.

Endpoints and the console

With the Dev Bar on, php-via serves these under /_via; with it off they all answer 404.

EndpointPurpose
GET /_viaThe Dev Console: the Dev Bar full-screen
GET /_via/streamSSE stream of trace and log events
GET /_via/scopesJSON snapshot for the Scopes panel
GET /_via/statsJSON snapshot for the Stats panel
POST /_via/signalSignal write, refused unless writes are on
POST /_via/resetClears the worker's trace buffer, refused outside dev mode
GET /_via/devbar.js, /_via/devbar.cssThe overlay's assets

The two POST endpoints check the Origin header as actions do. The overlay is a web component with its own shadow DOM, so your page's CSS and Datastar do not reach it, and it survives morphs of the whole page. Open /_via for the console.

The trace and log buffers live in each worker, so with withWorkerNum() above 1 the overlay and the console show the activity of the worker their stream reached.

Server statistics

The Stats panel reads /_via/stats every 2 s while it is open, and stops when you switch to another panel. requests, avg_request_time, actions and sse_connections count for the whole server: page, static file and route() requests, actions where they run, and streams that opened. The other figures belong to the worker that answered, which the panel names, so with several workers they change with the worker a poll reaches. Like the other panels, it is there wherever the Dev Bar is, also outside dev mode.

In dev mode, GET /_stats returns JSON for the worker that answers: its contexts, the connected clients, render timings (render_stats), broadcast flushes (broadcast_stats), memory and uptime. Under runtime it lists the hook_flags in force, the AIO thread pool (aio_worker_num, aio_task_num) and the event loop lag, where a call that blocks the worker shows up. Outside dev mode it answers 404, since it lists client IPs. In code, $app->getStats() returns the same render and broadcast figures.

See also