TelemetryUi facade's [@method](https://github.com/method) annotation for resolveConnectionsUsing()
never gained the needsViewer parameter added in 1.4.0, so a static analyser
flagged every correct call as passing an unknown argument.
connectionResolverNeedsViewer() is annotated too.Contracts\ProbesConnection. A driver can now answer
"is this connection actually usable?" without running a dashboard query, via
ConnectionManager::probe($name). The result is a classified ProbeResult
(BackendStatus: reachable / TLS / unauthorized / not-found / unexpected-API),
because "it didn't work" is useless — a wrong hostname, an untrusted CA, a bad
token and a URL pointing at the wrong product need four different fixes.
Implemented by the Tempo, Loki, Prometheus and Mimir drivers, each checking the
API shape, so a Loki URL pasted into the traces field is caught up front
rather than failing on every card. probe() never throws — a malformed config
comes back as a failed result.verify: true (default),
a path to a custom CA bundle for an internal PKI or self-signed certificate, or
false to skip verification. Env keys TELEMETRY_UI_{METRICS,TEMPO,LOKI}_CA_BUNDLE.
Only an explicit boolean false disables verification — a stringy "false"
from an env var is read as a CA path and fails closed, rather than silently
downgrading TLS on a typo.Contracts\WritesToBackend. A marker making the read-only posture
checkable instead of claimed: no telemetry read driver implements it, and
CreatesIssues now extends it. A host that promises "this never writes to your
stores" can assert that, rather than documenting it.resolveConnectionsUsing(..., needsViewer: false). For hosts with no
authentication at all, where the connection is a property of the process rather
than of a user. The default stays viewer-gated, so existing multi-tenant
resolvers — which dereference the user — are never handed null.SourceException now carries a BackendStatus, classified at the throw site
where the HTTP status and transport error are still available, instead of
leaving callers to pattern-match on message text.final from the connector classes per the house rule that package
classes stay open to extension (ApiClient, ConnectionManager,
ResolvedConnections, SourceException, TempoSource, LokiSource,
MimirSource).parent, instead of leaving the whole
navigation with nothing active.:stats instead of :items), so the headline was silently dropped and the
page rendered as a bare table.cboxdk/laravel-telemetry ^1.0 (was ^0.3.0). Aligns the dashboard
with the now-stable telemetry 1.0 line — the old ^0.3.0 constraint excluded
telemetry 0.4 and 1.0, so a fresh install pinned an older telemetry. No
dashboard code changes; verified against telemetry 1.0.0 (326 tests green).A visual overhaul plus New-Relic-style database dashboards. No PHP API changes —
the MetricsSource / TracesSource / LogsSource contracts and every result
DTO are unchanged from 1.0.0, so drivers built for 1.0 keep working untouched.
<select> (scope, period,
auto-refresh, and all per-card filters). Type-ahead filtering, full keyboard
navigation (↑/↓/Enter/Esc), and a selected-state check. Each wraps a hidden
native <select> so wire:model.live, x-model, and form navigation keep
working unchanged.⌘.). Both states persist in
localStorage.--tui-* CSS
variables are now remapped onto Cbox tokens, and the filter/scope pickers
no longer render a visible native <select> (a hidden one is retained for
binding). If you overrode --tui-* values or styled those selects directly,
review your overrides — no code migration is required.Backend-neutral query IR — the read side no longer speaks PromQL/TraceQL/LogQL
strings directly, which opens the dashboard to non-LGTM backends (see the
companion cboxdk/laravel-telemetry-store for a native ClickHouse store).
MetricsSource / TracesSource / LogsSource contracts now
take typed query objects (MetricQuery / TraceQuery / LogQuery) instead of
dialect strings. Result DTOs are unchanged. Custom drivers must implement the
new signatures — see UPGRADE.md.Queries\Ir\* (query objects, matchers, conditions) and Queries\Compilers\*
(Promql/Traceql/Logql compilers). Cards build the IR; each driver
compiles it to its dialect. A *::raw() escape hatch covers the few
dialect-only cases (nested aggregation, config-driven exporter queries).TelemetryUi::restrictScopeUsing(), or statically with no code via
telemetry-ui.scope.lock / TELEMETRY_UI_LOCK_SERVICES /
TELEMETRY_UI_LOCK_ENVIRONMENTS. The picker now reflects the lock: it offers
only allowed values, drops "All" for a locked dimension, and hides a picker
locked to a single value entirely. Enforcement stays at query time.telemetry-ui.analytics.dimensions.telemetry-ui.analytics.internal_hosts.telemetry.analytics.utm capture — with a single empty state until it's on.cboxdk/laravel-telemetry ^0.3.0 for the UTM / campaign capture
the Campaigns card and channel enrichment read.Dashboard cards drill into their pages. Cards that summarise a
dedicated page (Requests activity/duration, Exceptions, Jobs, queue and
autoscale cards) gain a "Requests →"-style header link when rendered on
the dashboard or as an embedded widget, carrying the active
period/service/env scope. On the card's own page the link is suppressed.
Package cards opt in by setting protected ?string $drillPage = 'my-page'.
Hide chart annotations per type. The header gains a ⚑ toggle listing
the configured marker types (Deploy, Incident, …) with a checkbox each,
plus a show/hide-all master switch — uncheck the noisy ones and every
chart drops those lines instantly. Purely client-side: cards always ship
the full annotation set and the charts filter marker lines by kind, so
toggling costs zero backend queries. The choice sits in the URL
(ann_off), so it survives navigation and deep links.
Grafana-style relative time ranges. ?from=now-1h&to=now,
now-7d, now+30m … (units s/m/h/d/w/M/y) work everywhere from/to
do — evaluated at view time, so a shared relative link always shows the
trailing window instead of a frozen one. Plain unix seconds still work,
and the header shows relative expressions verbatim.
Livewire updates carry their component everywhere. With
cboxdk/laravel-telemetry ≥ 0.2.1, POST /livewire/update is named
livewire:{component} (batched updates: livewire:batch), so the routes
table groups per component instead of lumping thousands of opaque updates
into one row. The request log shows the component(s) behind each update
inline, and the Livewire page gains the Requests page's grouping/live-tail
pair: a per-component table and a scoped live request log.
Collapsible sidebar navigation. The nav groups (Activity, Monitoring, Statamic, …) collapse to chevrons so a long page list fits on one screen; only the active group opens by default and the choice persists in localStorage. Top-level items (Dashboard, Traces, Issues) stay visible.
Frontend page rows drill into their traces. Core Web Vitals and Page
performance rows open the browser→backend traces for that URL path, matched
on span.url.path, carrying the active scope.
increase()
(Prometheus never sees the 0→first-value jump). Cards now count series
births too, so "Scale down: 1" shows up the moment the first scale-down
ever happens.queue_autoscale_cluster_*
gauges only exist in cluster mode, and the card now says so.<div> was the
card's grid item, so the span 2 on the inner card never reached the grid
and the log was squeezed into one column. The wrapper now uses
display: contents.direction label carries up / down (the WorkersScaled action), not
scale_up / scale_down; the card now groups by the label instead of
filtering on guessed values. Verified against live production series —
as are the rest of the autoscale names, including
queue_autoscale_sla_breach_ratio and
queue_autoscale_sla_predicted_pickup_seconds.deployment_environment_name in the Loki stream selector, but backends that
index only service_name as a stream label (e.g. otel-lgtm) carry the
environment as structured metadata — so a selected environment silently
matched nothing and Analytics, the log viewer, unified errors and annotations
all returned zero. The environment is now a pipeline label filter
({service_name="…"} | deployment_environment_name="…"), correct whether the
backend indexes it as a stream label or not. The analytics Countries/Devices
empty states now name the emitter flags (TELEMETRY_ANALYTICS_GEO /
TELEMETRY_ANALYTICS_UA) that populate them.queue_metrics_queue_throughput_per_minute;
the OTLP collector translates the {jobs}/min unit to a _per_min suffix
(verified against live data), so the Throughput card, the queues table's
Jobs/min column and the queue-detail header matched nothing. Now they
query queue_metrics_queue_throughput_per_min.queue_metrics_.*) for fleets running
cboxdk/laravel-queue-metrics' OpenTelemetry integration: backlog by
state (pending/scheduled/reserved), per-queue throughput, oldest-job age,
busy/idle worker fleet with utilization, and a per-queue table with
backlog-trend sparklines. Each queue drills into a queue-detail page:
headline numbers, backlog and throughput for that queue, the autoscaler's
target-vs-active steering, and the job classes running on it (each linking
on to its job-detail page).queue_autoscale_.*) for
cboxdk/laravel-queue-autoscale v3.11+: target vs active workers,
executed scaling actions by direction, SLA health (predicted pickup,
queues in breach, breach transitions) and cluster capacity
(workers/required/capacity, managers, utilization, recommended hosts).cboxdk/laravel-telemetry ^0.2.0 — its first stable release
(was ^0.1.0-alpha.3).error-detail?group=…, the drawer's "Full page" button):
header with events / users / first seen / last seen, an events trend
chart with the deploy/change markers drawn on top (release markers,
Sentry-style), tag distributions (host, environment, release,
service, user — "is it one box, one release, one customer?"), and the
full deep-dive: request strip, root-cause hints, source context,
stacktrace and recent occurrences. Drawer and page share one
per-request-memoized ErrorGroupReport, so the page's four cards cost
one set of backend queries.enduser.id laravel-telemetry ≥ alpha.18 stamps on exception records):
a Users column on the errors list and a "users affected" fact on the
group panel.telemetry-ui.host-services,
{host} token): a probe that returns nothing simply doesn't render, so
listing exporters you don't run is free — and adding your own is a
config entry, not code.host.name that served the
trace (linked to the Hosts page) and the service, or "all hosts" when
the resource carries no host and the queries aggregate service-wide.
host.name also joined the resource rows in the span attribute panel.db.queries counter, no exporter required.
MySQL/Postgres exporter probes remain the path to real health stats.observed badge instead of
up/down — traffic measured by the app proves usage, not health — plus a
note pointing at the exporter that would unlock full monitoring.The route page's Paths card leaked every path in the backend. It used Tempo's tagValues with a filter that v1 quietly ignores — a route detail listed unrelated URLs from the whole fleet. Paths now aggregate from the route's own spans with real numbers per path (requests, avg, max, 4xx/5xx) — a row tails that path in the request log, and ⇄ opens the newest request's story.
Charts no longer stick at the width they measured mid-render. The
ECharts instance now lives outside Alpine's reactive proxy (a proxied
instance silently breaks resize()), and a ResizeObserver keeps the
layout in step with the container — Livewire streaming a card in at
zero width, sidebar/drawer toggles and orientation changes all heal.
Mobile-friendly layout — below 768px the sidebar becomes an off-canvas drawer behind a sticky topbar hamburger, header controls wrap, the trace waterfall drops the per-row service chip so span names stay readable, the custom-range popover anchors to the viewport, the trace drawer goes full-width, and inputs are sized to avoid iOS focus zoom.
laravel-telemetry v0.1.0-alpha.16 support — five new auto-detected sidebar pages plus cards, each appearing only when the fleet emits the signals:
horizon_*): worker processes per supervisor with
paused/supervisor gauges, and an incidents chart (long waits, restarts,
OOM kills, migrated jobs).reverb_*): active WebSocket connections per app with
subscribers by channel type and pruned-connection counts, plus message
throughput sent vs received.feature_*, Pennant): checks by flag with active
share and per-result badges, and a warning strip for checks against
unregistered flags.storage_operations_*): Flysystem disk operations per
minute by type, with per-disk totals.livewire_*): mounted vs hydrated components per minute,
and a slowest-components table off the render/update/call detail spans.web-vitals spans, toned on Google's
thresholds.db.query.duplicate_detected log events — query text, traces affected,
worst repeat count, trace link.Error-group detail drawer (Sentry-style issue view) — clicking a row on
the unified Errors card now opens a drawer with the exception's message,
occurrence stats (count, first/last seen, source), the latest occurrence's
stacktrace and source context, a prefilled "+ ticket" compose button and
a recent-occurrences table whose trace links stack onto the drawer.
Deep-linkable via ?exception=<group>; searches are forced inside the
viewer's tenancy scope lock.
Cache purge annotations — two new built-in markers: cache_purge
(app-agnostic, emit via php artisan telemetry-ui:annotate cache_purge --id=redis) and statamic_cache_purge, which matches the
statamic.cache.purge events cboxdk/statamic-telemetry emits on every
stache/static/glide clear — so Statamic purges show up as chart lines and
timeline events with no wiring.
Issues across multiple repos — connections.issues may now be a list of
trackers (frontend, api, sidecar, …), each with a label. The Issues page
aggregates them newest-first, tags each row with its repo, and adds a repo
filter. A single connection still works unchanged.
Manual refresh button in the toolbar, next to the auto-refresh control.
Trace list reads like requests — rows now show the HTTP method, route and
status (error-highlighted) off the root span, plus a web badge for RUM
traces, instead of a bare span name.
Row drill-down on Analytics & Frontend — a page row opens every trace for that path, which spans the browser page load and the backend request it triggered (frontend → backend in one waterfall).
?q= on the Traces page ran verbatim, bypassing the tenancy
lock; it is now forced into the viewer's allowed services (drill-down links
go through the scoped builder too).telemetry-ui.throttle (120/min per IP) as the pages, so
a busy dashboard — auto-refresh, several tabs, a shared office IP — could
429 the bundle itself and take every chart down
(telemetryUiChart is not defined). Version-stamped immutable assets now
skip the throttle, the same way they already skip the auth gate.span.exception.group, but laravel-telemetry records backend exceptions
as span events (and as structured log records) — the attribute never
exists on spans, so the card was permanently empty in production. It now
reads the structured exception records from Loki (authoritative — present
even when the trace is sampled away) and merges in browser exception spans
from Tempo, fingerprinted read-side with the same algorithm the backend
uses (browser ingest doesn't stamp exception.group).TelemetryUi::setCards() / removeCard() now affect the dashboard page's
config-declared cards (they previously no-op'd there).connections.issues list is skipped
instead of 500-ing the Issues page; a partial per-tracker failure shows a
warning banner rather than silently dropping that repo.hasIssues() is resolver-aware
so page/gate decisions match the tracker actually resolved.url(...), and an
array-shaped ?service[]= no longer corrupts the DashboardViewed audit scope.DashboardViewed (user + page + scope, for audit and
usage metering) and BackendQueried (url + method + duration + ok, for backend
load metering per tenant) let a host hook the dashboard without patching it.telemetry-ui.brand config sets the sidebar
name, logo and accent colour; views are namespaced (telemetry-ui::) so a
host can override any of them.TelemetryUi::resolveConnectionsUsing(fn ($user) => [...])
resolves connection config per viewer, so a hosted multi-tenant install can
point each tenant at their own Mimir/Tempo/Loki (or a shared backend behind a
per-tenant X-Scope-OrgID). Omitted connections fall back to the static
config; drivers are cached by config, so one tenant never gets another's under
Octane. See authorization.TelemetryUi::restrictScopeUsing(fn ($user) => [...])
locks a viewer to a subset of services and/or environments, for embedding the
dashboard in an app. The scope switcher only offers the allowed values and
every query is forced into the lock server-side — a blank or hand-edited
?service= can't widen past it (one allowed service → service_name="x",
several → a service_name=~"a|b" alternation), across metrics, traces and
logs. Resolved per request. See the authorization doc.Analytics page (visit analytics) — a privacy-first traffic dashboard built
on the emitter's unsampled analytics.page_view stream: a page-views trend
chart (with deploy annotations), unique visitors (the cookieless daily
session hash — no cookies, no stored IP), views-per-visit, bounce rate
(single-page-view sessions) and average engagement time (from
analytics.engagement events), top pages with distinct visitors, and a
sources/audience
breakdown (referrers, and — when the emitter's geo/User-Agent enrichment is on
— countries and devices). Trace/Loki-sourced so it's exact for low-traffic
sites and a bounded sample at scale (the eventual answer being a ClickHouse
sink behind the same cards). Also: the Loki driver now surfaces per-entry
structured metadata, so high-cardinality OTLP log attributes (the visit
dimensions) are readable instead of dropped.
Frontend page (RUM) — a new Monitoring page for real-user browser data:
Page performance (navigation timings per page — loads, avg load, TTFB,
DOM-interactive, from the document.load spans) and Failed browser
requests (fetch/XHR calls that 5xx'd or errored, grouped by URL, each row
opening a representative trace where a same-origin failure continues into the
backend span that caused it). Trace-sourced (no RUM metric exists), bounded
sample.
Unified errors list (frontend + backend) — a new lead card on the
Exceptions page groups every error by exception.group, the Sentry-style
fingerprint (class + top in-app frame) that both the backend handler and the
browser SDK stamp with the same algorithm. A JS TypeError and a PHP
exception that are "the same issue" collapse into one row tagged
web/server/full-stack, with an occurrence count and last-seen; clicking
a row opens a representative trace (→ waterfall + host context), and "all"
jumps to every trace for that fingerprint. Trace-sourced (metrics can't unify
— frontend errors exist only as spans), so counts are over a bounded recent
sample.
Frontend / RUM spans in the unified trace — browser spans emitted by
cboxdk/laravel-telemetry's frontend proxy (alpha.6/7) now read as first-class
frontend rows. They share the backend's service.name, so the per-span
server-stamped browser=true attribute is the marker: browser spans get a
web badge in the waterfall, document.load shows its RUM timings (TTFB,
DOM), and browser fetch spans render their URL + status. Trace search
gains a Source filter (frontend/backend) that scopes on span.browser.
Because the browser continues the backend's traceparent, a page load, its
fetches and the server spans they trigger already nest into one waterfall —
end-to-end frontend→backend on open data.
Dimensional drill-down / filtering (Grafana-style) — every span/resource
attribute in the trace properties window (host, user, team, client IP,
deployment, method — whatever the app emits) is a click-to-filter link that
scopes Traces to { .key = "value" }. Plus a new Hosts page listing
every host/server reporting telemetry (request volume, errors, CPU, memory),
each row filtering requests to that host.
Purpose-built detail pages with progressive drill-down — clicking a row
opens a dedicated detail page instead of a pre-filtered trace search, à la
Nightwatch, for routes, jobs and exceptions. Each shows the entity's own
numbers scoped to it, and drills deeper: a route detail goes throughput →
latency → exact status codes → its individual traces (→ waterfall + host
context). Built on a "hidden page" concept (routable + rendered, out of the
sidebar), a scopeMatchers() card hook and per-entity ScopesTo* traits, so
the overview cards are reused scoped to one entity — the pattern extends to
hosts, queries, etc. cheaply.
MCP server — php artisan mcp:start telemetry-ui serves metrics, traces,
logs and the correlation/analysis tools over the Model Context Protocol,
built on the first-party laravel/mcp package, so an agent (Claude Desktop,
Cursor, …) can query the stack directly for incident RCA. Six read-only
Server\Tools, including trace_context (a trace plus the host/runtime
signals around it, flagged against normal). Same read drivers the dashboard
uses.
Remote MCP over HTTP with OAuth + DCR — set TELEMETRY_UI_MCP_WEB=true
(and install laravel/passport) to expose the server over HTTP behind
auth:api, with the OAuth 2.1 authorization server and Dynamic Client
Registration endpoint that laravel/mcp provides — no custom OAuth code.
Off by default; Passport stays optional.
Signal correlation — a trace now shows the host and runtime signals
recorded around it (CPU, load, memory, network, process RSS) in a context
strip beside the waterfall, scoped by service + host and the trace's time
window. This is the thing an app-only monitor can't do: the same Prometheus
scrapes system_*/process_* — and node_exporter, mysqld_exporter, … when
present — right next to the app. Config-driven and fail-open per signal
(telemetry-ui.context.signals); a new headless Analysis\SignalContext
is the reusable foundation.
"What was different" — each context signal also carries its baseline (the typical value for that scope over a longer lookback), so a tile reads "Host CPU 95% (typical 30%)" and flags outliers. Answers the "was the box busted?" question at a glance, without ML — just an honest comparison to normal.
php artisan telemetry-ui:check — probes each configured connection with its
cheapest read and reports OK/FAIL/not-configured; exits non-zero on failure
so it doubles as a deploy healthcheck.
Annotation writing — php artisan telemetry-ui:annotate <marker> emits a
marker (deploy, incident, scaling, migration, feature, version — or your own)
through the telemetry pipeline into Loki, where it renders as a vertical line
on every chart. No local state: the same store the dashboard already reads.
cboxdk/laravel-telemetry is now a hard dependency (it provides the write
path, and the dashboard instruments its own stack).
Proactive auto-version annotations — php artisan telemetry-ui:scan-versions
(schedule it) detects a laravel_version that's live in the metrics but
un-annotated and marks it, so an un-announced deploy still lands on the
charts. Stateless: it dedups against the version annotations already in Loki.
Whole-row click targets on the routes, jobs, facet, slow-query, trace-search,
outgoing and exceptions tables — the entire row drills into the matching
traces (or opens the trace drawer / matching issues), not just the small
link. cmd/ctrl-click opens in a new tab. Outgoing rows filter traces by
server.address; exception rows jump to their matching issues (or the
scoped error traces when no tracker is configured).
on-load) instead of rendering eagerly,
so the page shell paints instantly and a slow backend query on one card no
longer blocks the whole page; each card loads in its own parallel request.trigger: 'axis') and dragging realigns the range.errors array) as a SourceException
instead of silently returning an empty issue list.NaN/+Inf/-Inf (which Prometheus
serializes as strings) were cast to a misleading 0.0. They are now dropped
so gauges/ratios show a gap instead of a false zero, and the scalar
result branch no longer risks a raw TypeError past the SourceException
boundary.telemetry-ui.fleet.ttl / TELEMETRY_UI_FLEET_TTL), matching the other
cache TTLs.ConnectionManager::client() is now public so custom drivers registered via
extend() can reuse the configured ApiClient (auth, tenancy, cache,
retries) instead of building one by hand.viewTelemetryUi gate is now re-run on
Livewire updates (card/drawer actions POST to /livewire/update, which
previously skipped it — the gate was only enforced at page load). The gate
also receives the page slug, so an app can restrict individual pages
(e.g. the PII-heavy Logs/Users) without closing the whole dashboard — denied
pages 403 and drop from the sidebar/palette. And a new manageTelemetryUi
ability gates write actions (creating tracker issues), checked server-side and
hiding the compose UI, so a read-only viewer can't file tickets (it falls back
to the view gate, so existing setups are unchanged). See the new
authorization doc.SourceException now carries a generic
user-facing message and a separate detail; ApiClient logs the full detail
server-side (the dashboard gate may be opened to semi-trusted operators).laravel/passport is absent, instead of registering a half-configured
authorization server. mcp.web.middleware documents that auth:api is the
only guard on that endpoint.tagValues lookups carry a time window + limit, and the annotation writer
can no longer crash a command or the scan-versions cron on an emit failure.First alpha. A Livewire + ECharts observability dashboard querying Tempo
(TraceQL), Loki (LogQL) and Prometheus/Mimir (PromQL) directly — a companion
to cboxdk/laravel-telemetry.
MetricsSource/TracesSource/LogsSource/IssuesSource
contracts and Prometheus, Mimir, Tempo, Loki, GitHub, Sentry and Linear
drivers, resolved lazily through a ConnectionManager with extend().telemetry-ui.cache.ttl seconds (default 5, override per connection) so a
busy dashboard with many cards and auto-refresh does not hammer
Prometheus/Tempo/Loki. Only plain arrays are cached, never DTOs.telemetry-ui.retries).telemetry-ui.throttle
(default 120,1).How can I help you explore Laravel packages today?