The dashboard lands on #/overview: request p50/p95 latency and error rate, a recent-duration
chart, per-type entry counts, recent exception families, and a slow-query leaderboard, all scoped
to the selected application. Every entry type has its own page, including logs, events, views,
health checks, broadcasts, and a monitored-tags management screen.
Search and filtering:
- Free-text search over serialized entry content (
text=onGET <path>/api/entries), backed by FTS5 onsqlite-localand portableLIKEelsewhere. Case-insensitive and literal, including%and_. - Multiple exact tags with AND semantics (repeated
tag=parameters). - Inclusive time ranges (
from=/to=, ISO datetimes) with 15m/1h/24h quick presets. - Server-side predicates for slow queries (
tag=slow), request status classes (tag=status:2xx…tag=status:5xx), and repeated query families (tag=n+1).
All filters persist in the hash-route query string, so filtered views are shareable. Every entry
is deep-linkable at #/entries/<uuid>, and detail drawers offer copy-link actions for entries and
batches. Entry details render identically on index pages, global search, batch timelines, and
direct links. Request and ambient batches export as versioned periscope.batch JSON; mail entries
download as .eml.
The header stays minimal: page title, content search, an application selector (shown only when
the store holds more than one application), and an options menu containing pause/resume, a scoped
clear, the persisted light/dark/system theme choice, and the shortcut reference. While recording
is paused, a “Paused” chip appears next to the title; clicking it resumes.
Keyboard shortcuts: / focuses search, j/k move across rows, Esc closes
the open drawer, ⌘/Ctrl+B toggles the sidebar, and ? lists them all. Live updates stream over
SSE with visibility-aware polling as fallback; dashboard.sseMaxClients (default 5) bounds
concurrent stream clients.
Dashboard security
The dashboard and JSON/SSE API live below dashboard.path. Every dashboard request passes the
environment gate and then dashboard.authorize. The default authorizer denies requests in
production; override dashboard.authorize explicitly to enable access there.
Request details surface repeated query-family warnings at dashboard.nPlusOneThreshold (families
meeting the threshold are also persisted with the exact n+1 tag, so they are filterable and
monitorable), an active OpenTelemetry trace ID when @opentelemetry/api is installed, Inertia
component and prop-key metadata when responses carry the X-Inertia header, and a JSON batch
export suitable for bug reports. Exports contain the application label and JSON-safe entries; they
never initiate an outbound request.
For a deliberately exposed non-development environment, require an application-specific identity:
export default defineConfig({
enabledIn: ['development', 'staging'],
dashboard: {
path: '/internal/periscope',
authorize: async ({ auth }) => {
await auth.check()
return auth.user?.email === 'operator@example.com'
},
},
})Do not use a guessable URL as authorization. Terminate TLS at the application or a trusted proxy, protect the route with the same identity controls as other operational tools, and keep the production environment disabled unless an incident workflow requires it.
Mail HTML is sanitized before rendering, then placed in an iframe with an empty sandbox and a
no-referrer policy. Remote images, scripts, forms, embedded content, event handlers, refreshes,
and network-capable CSS are removed.
Production sampling
Periscope remains off in production by default. If an incident requires temporary recording, use strict authorization, aggressive sampling, low caps, redaction, short retention, and an explicit enablement window. The complete recipe and shutdown checklist are in Operations.