Releases: cboxdk/laravel-telemetry-ui
Release list
v0.4.0
Added
- Scope lock (tenancy). Constrain the whole dashboard to a fixed set of
services / environments — dynamically per user via
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. - Page-detail show pages. Analytics and Frontend rows now open a dedicated
page-detail scoped to that one URL path — traffic (views, visitors,
referrers, countries, devices), real-user performance (Core Web Vitals + load
timings), the page's browser→backend traces, and its browser errors — instead
of dumping into a pre-filtered trace search. - Richer visitor breakdowns. The analytics "Sources & audience" card gains
Sources, Regions, Cities, Operating systems and Browsers alongside Referrers,
Countries and Devices, each toggleable viatelemetry-ui.analytics.dimensions. - Marketing channel — a first-class, zero-cardinality dimension. Visits are
classified into Direct / Organic search / Social / Email / Paid / Referral /
Internal, derived at read time from the referrer (and UTM medium + paid
click-id once the emitter captures them), so it costs no ingest cardinality.
Configure your own hosts withtelemetry-ui.analytics.internal_hosts. - Campaigns card. UTM campaign attribution (campaign / source / medium, and
higher-cardinality content / term) from the emitter's
telemetry.analytics.utmcapture — with a single empty state until it's on. - Cardinality guide in the analytics cookbook: per-dimension high/low
classification, the capture-vs-surface control split, and the ClickHouse path.
Fixed
- Chart-annotation toggle no longer shows a duplicate "Cache purge" (the
Statamic marker is now its own labelled, distinctly-coloured entry) and its
rows/labels align in one clean column.
Changed
- Requires
cboxdk/laravel-telemetry^0.3.0 for the UTM / campaign capture
the Campaigns card and channel enrichment read.
v0.3.0
Added
-
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 settingprotected ?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 everywherefrom/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/updateis 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
onspan.url.path, carrying the active scope.
Fixed
- Drill links no longer appear on a card's own page. Cards lazy-load in
a follow-up Livewire request where the page route param is gone, so every
card thought it was on the dashboard and grew a self-referencing
"Autoscale →" link. The page view now passes the page slug into each card
at mount. - Sparse counters no longer read as zero. Scaling actions and SLA
breaches born inside the selected window were invisible toincrease()
(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. - The Cluster card explains itself on single-host installs instead of
showing misleading "0 managers / 0%" stats — thequeue_autoscale_cluster_*
gauges only exist in cluster mode, and the card now says so. - Routes ⇄ Request log toggle no longer reloads the page. The toggle was
a plain link (full page load); it is now a Livewire event both sibling
cards listen to, so the swap is instant and keeps scroll/filter state. - Request log renders full-width. Its live-poll wrapper
<div>was the
card's grid item, so thespan 2on the inner card never reached the grid
and the log was squeezed into one column. The wrapper now uses
display: contents. - Live tail is on by default on the request log.
- Scaling actions card matched nothing on real data. The autoscaler's
directionlabel carriesup/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_ratioand
queue_autoscale_sla_predicted_pickup_seconds. - Environment scope now works on every log-based card. The scope put
deployment_environment_namein the Loki stream selector, but backends that
index onlyservice_nameas 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. - Rootless traces no longer read as a perpetual "Loading…". An orphaned
browser page-view trace (no backend root span) showed "Loading…" forever in
the drawer; it is now labelled (the span name, "Unnamed trace" or "Trace not
found") since the fetch is already complete by render time.
v0.2.1 — fix queue throughput metric name
Fixed
- Queue throughput cards queried
queue_metrics_queue_throughput_per_minute; the OTLP collector translates the{jobs}/minunit to a_per_minsuffix (verified against live production data), so the Throughput card, the queues table's Jobs/min column and the queue-detail header matched nothing. Now they queryqueue_metrics_queue_throughput_per_min. All other derived metric names and labels verified correct against live data.
Full Changelog: v0.2.0...v0.2.1
v0.2.0 — Queues & Autoscale pages
Added
- Queues page (autodetected via
queue_metrics_.*) for fleets runningcboxdk/laravel-queue-metrics' OpenTelemetry integration (v3.2.0+): 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). - Autoscale page (autodetected via
queue_autoscale_.*) forcboxdk/laravel-queue-autoscalev3.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).
Changed
- Require
cboxdk/laravel-telemetry^0.2.0— its first stable release (was^0.1.0-alpha.3).
Full Changelog: v0.1.0-alpha.6...v0.2.0
v0.1.0-alpha.6
Added
- Copy an issue as Markdown for an LLM. The issue page's Actions &
context sidebar gains a ⧉ Copy for LLM button that puts a
self-contained Markdown brief on the clipboard — exception, message,
location, occurrences/users, environment/release/host, the request that
hit it, the suspect change, the releases it rode in on and the freshest
stacktrace — ready to paste into a model with "help me fix this". No
tracker write access required.
Changed
- Optional sidebar groups follow the selected service. Schema detection
now scopes to the active service/environment, so a group like Statamic
(or Horizon, Reverb, …) only appears when that service emits its metrics —
not because some other service in the fleet does. "All services" still
detects fleet-wide, and the tenancy lock still bounds what a viewer can see.
v0.1.0-alpha.5
Added
- Issue rows go straight to the issue page — no drawer detour — and
the page gains a Sentry-style Actions & context sidebar next to the
trend: a prefilled "+ Create ticket" button, the tracker tickets that
already mention this exception (searched live), the key facts (first/
last seen, users, env, release, host → its page, throw site) and the
suspect change event. - Request log with live tail. The Requests page's Routes card gains a
toggle sibling: a request LOG — individual requests, newest first, with
time, method+path, status badge, user, client IP and duration. Filter
by user id, client IP, path or status class (each user/IP cell is
click-to-tail), hit ● Live and the list re-polls every few seconds —
production debugging for "what is this user hitting right now?". Every
row opens the readable request story in the pane. - Traces read like requests now — the waterfall is the last resort.
Opening a trace (pane or full page) tells the story instead of dumping
spans: request facts (method, route, path+query, status, client IP,
user, user agent, body sizes), cost totals off the root tallies
(queries + query time, N+1 count, cache ops, redis commands, models
hydrated, views, CPU time, memory), then one readable section per
concern — Database (statements slowest-first, N+1 called out),
Upstream calls (URL + status + duration), Cache (hit/miss/write
summary + keys), Redis, Dispatched jobs, Views, Storage,
captured Headers (request/response) and Logs written during the
request (trace-correlated). The raw span waterfall collapses behind a
"Raw trace — N spans" toggle. Job/command traces adapt automatically. - Purpose-built trace filters: status-code class (2xx–5xx), path
contains and client IP join status/source/route/name/duration — all
URL-synced, all composing scoped TraceQL under the hood. - Full issue page (Sentry-style show view). Every error group now has
its own page (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-memoizedErrorGroupReport, so the page's four cards cost
one set of backend queries. - Users affected. Error groups now count distinct users (from the
enduser.idlaravel-telemetry ≥ alpha.18 stamps on exception records):
a Users column on the errors list and a "users affected" fact on the
group panel. - Errors list filters. Free-text filter over type/message and a
source selector (server / web / full-stack), both URL-synced. - Scope moved to the top header. Service and environment now sit
next to the period picker on every page — Sentry's top-bar pattern —
and the sidebar is pure navigation. - Full-color card borders. The context tiles and root-cause box wear
their accent color as a full border instead of a left edge. - Sentry-style errors list. Every group row now carries its in-period
trend sparkline, first seen and last seen, a NEW badge for groups
born within 24 hours, and a sort control (events / last seen / first
seen). First-seen looks beyond the page period (min 7 days) so it means
what it says; counts and trends stay period-scoped. - Root-cause hints on the error-group panel. The change event (deploy,
migration, feature flag, …) closest before the group's first occurrence
— within 48h — is named as the suspect ("Deploy v9.1.0 at 14:02 — first
seen 18 minutes later"), and the panel shows which releases the sampled
occurrences carry, with an "only this release" badge when every one
points at a single release.
Fixed
- Annotation callout is anchored to its marker line again. The deploy/
change callout now sits hard against the line it describes — flipping to
the line's other side near the chart edge (where deploys cluster) instead
of detaching to a fixed corner — with a caret pointing back at the line.
The raw axis tooltip no longer stacks on top of it: the series readout is
suppressed while the callout is open, and the callout renders above it
regardless. Hovering into the callout keeps it open to reach Open
trace, so the click-to-pin affordance is gone.
v0.1.0-alpha.4
Highlights
The drawer is now a docked properties pane. On wide screens it pushes the page aside instead of covering it — no backdrop, the page stays interactive, and selecting another row swaps the pane's content (links inside the pane still stack with back-navigation). It also opens instantly with a skeleton while the data loads.
Interactive, cluster-aware annotations. Hovering a marker line opens a callout anchored to the line (the pointer can move into it); clicking pins the same callout in the same place. Horizontal rollouts fold into one marker: 200 servers deploying = one line with ×200, the rollout span (first → last host) and the covered hosts.
Exception groups link back to the request. Env / release / host facts (host → its detail page) plus a "Latest occurrence" strip off the trace root: method + route (→ route detail page), status, user and the request trace.
Host detail page. Headline CPU/memory/load/requests, host-scoped system charts, and a "Services on this host" card fed by the services' own Prometheus exporters (mysqld/redis/postgres/node probes as defaults) — plus exporter-less app-side Database and Redis sections built on laravel-telemetry's db.queries (alpha.17) and redis.commands counters, honestly badged observed.
Issues page = errors + tickets. The unified error groups sit above the tracker list, and issue/PR bodies render as formatted markdown through a strict-allowlist sanitizer (external content, never executable).
Context strip names its scope — the exact host (linked) and service the tiles describe, or "all hosts" when that's the truth.
Fixed
- Charts no longer stick at the width they measured mid-render (ECharts instance out of Alpine's reactive proxy + a ResizeObserver).
Requires cboxdk/laravel-telemetry ≥ 0.1.0-alpha.17 for the app-side Database host section. See CHANGELOG.md for the full list.
v0.1.0-alpha.3
Highlights
Sentry-style error-group drawer. Click any row on the unified Errors card for the full issue view: message, occurrence stats (count, first/last seen, source), the latest stacktrace with highlighted source context, a prefilled "+ ticket" compose button, and a recent-occurrences table whose trace links stack onto the drawer. Deep-linkable via ?exception=<group>, always inside the tenancy scope lock.
The Errors card now works against real data. It previously searched Tempo for span.exception.group, an attribute laravel-telemetry never puts on spans (backend exceptions are span events + structured Loki records) — so the card was permanently empty in production. It now reads the Loki exception records and merges in browser exception spans, fingerprinted read-side with the backend's own algorithm.
laravel-telemetry v0.1.0-alpha.16 support. Five auto-detected pages — Horizon, Reverb, Feature Flags (Pennant), Storage, Livewire — plus Rate limiting on Requests, real-user Core Web Vitals (p75 LCP/CLS/INP) on Frontend, Duplicate queries (N+1) on Queries, a CPU-profile strip on the trace view, and OTel span links (queue retries) as clickable linked-trace rows.
Mobile-friendly. Off-canvas sidebar behind a topbar hamburger, touch-sized controls, full-width trace drawer, no iOS focus-zoom.
Cache purge annotations. cache_purge (emit via telemetry-ui:annotate) and statamic_cache_purge, matching the events cboxdk/statamic-telemetry emits on every stache/static/glide clear.
Fixed
- Assets are exempt from the dashboard throttle — a rate-limited JS bundle no longer takes every chart down.
See CHANGELOG.md for the full list.
v0.1.0-alpha.2
Added
-
Analytics page (visit analytics) — a privacy-first traffic dashboard built
on the emitter's unsampledanalytics.page_viewstream: 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.engagementevents), 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 thedocument.loadspans) 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 byexception.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 JSTypeErrorand 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'sservice.name, so the per-span
server-stampedbrowser=trueattribute is the marker: browser spans get a
webbadge in the waterfall,document.loadshows its RUM timings (TTFB,
DOM), and browserfetchspans render their URL + status. Trace search
gains a Source filter (frontend/backend) that scopes onspan.browser.
Because the browser continues the backend'straceparent, 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), ascopeMatchers()card hook and per-entityScopesTo*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-uiserves metrics, traces,
logs and the correlation/analysis tools over the Model Context Protocol,
built on the first-partylaravel/mcppackage, so an agent (Claude Desktop,
Cursor, …) can query the stack directly for incident RCA. Six read-only
Server\Tools, includingtrace_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 installlaravel/passport) to expose the server over HTTP behind
auth:api, with the OAuth 2.1 authorization server and Dynamic Client
Registration endpoint thatlaravel/mcpprovides — 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
scrapessystem_*/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 headlessAnalysis\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-telemetryis 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 alaravel_versionthat'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).
Changed
- Dashboard cards now stream in (lazy
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.
Fixed
- Chart hover tooltips and drag-to-zoom both work now. The dataZoom brush was
kept permanently armed for drag-select, which put every chart in select-mode
and suppressed hover tooltips — so "no data on hover" and "zoom broken" were
the same bug. Replaced with a raw zrender drag-select (own selection band),
so hovering shows values (trigger: 'axis') and dragging realigns the range. - Linear now surfaces GraphQL errors (auth/permission/query failures, which
Linear returns as HTTP 200 with anerrorsarray) as aSourceException
instead of silently returning an empty issue list. - Prometheus/Mimir non-finite values:
NaN/+Inf/-Inf(which Prometheus
serializes as strings) were cast to a misleading0.0. They are now dropped
so gauges/ratios show a gap instead of a false zero, and thescalar
result branch no longer risks a rawTypeErrorpast theSourceException
boundary. - Fleet (sidebar service/environment) cache TTL is now config-wired
(telemetry-ui.fleet.ttl/TELEMETRY_UI_FLEET_TTL), matching the other
cache TTLs.
Changed
ConnectionManager::client()is now public so custom drivers registered via
extend()can reuse the configuredApiClient(auth, tenancy, cache,
retries) instead of building one by hand.
Security
- Finer-grained authorization. The
viewTelemetryUigate 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 newmanageTelemetryUi
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. - Backend failures no longer leak the internal endpoint, query string or raw
response body to the dashboard.SourceExceptionnow carries a generic
user-facing message and a separate detail;ApiClientlogs the full detail
server-side (the dashboard gate may be opened to semi-trusted operators). - The MCP web transport throws at boot when OAuth is enabled but
laravel/passportis absent, instead of registering a half-configured
authorization server.mcp.web.middlewaredocuments thatauth:apiis the
only guard on that endpoint. - MCP tools are bounded (row/series/window/limit caps + a dedicated throttle),
tagValueslookups carry a time window + limit, and th...