Web analytics & RUM
Web analytics & real-user monitoring
Because cboxdk/laravel-telemetry instruments both the server and the
browser and stitches them with one traceparent, this package can show visit
analytics that most tools can't — every page view is one row carrying behaviour
(referrer, device, engagement), performance (Web Vitals + backend timing),
errors, and real user identity, with drill-down to the exact trace.
Nothing here is on by default. It's an additive layer on top of the telemetry you already collect, and every piece is a separate flag.
Turn it on (in the emitter)
Analytics and RUM live in the emitting package. In your app's .env:
# Visit analytics — one unsampled analytics.page_view event per view.
TELEMETRY_ANALYTICS=true
TELEMETRY_ANALYTICS_SALT=change-me # salts the cookieless daily session hash
TELEMETRY_ANALYTICS_UA=true # parse User-Agent → device / browser / os
# TELEMETRY_ANALYTICS_GEO=true # needs a GeoLite2 .mmdb (optional dep)
# TELEMETRY_ANALYTICS_GEO_DB=/path/to/GeoLite2-Country.mmdb
# Browser RUM — page-load timings, fetch spans, JS errors, SPA views, engagement.
TELEMETRY_INGEST_SPANS=true
Then drop the browser SDK into your layout's <head> — it renders the
traceparent meta tag and the zero-build script:
<head>
…
@telemetryBrowser
</head>
That's it. Server-side page views start flowing immediately; browser events
start as soon as a page with @telemetryBrowser is loaded.
Privacy
Unique visitors are counted by session.id — a cookieless, daily-rotating
hash of ip + user-agent + host + salt. No cookie, no consent banner, and the
raw IP is never the grouping key (and needn't be stored). It rotates at
midnight, so uniques are per-day; cross-day retention is deliberately not
possible in this mode.
Where the data goes
analytics.page_viewandanalytics.engagementare emitted as unsampled OTLP log records (→ Loki). Unsampled matters: page views must never be undercounted, even when the full trace is tail-sampled away.- Browser RUM spans (
document.load,fetch …,exception) are traces (→ Tempo), taggedbrowser=true.
Point the emitter at your backend as usual — a local otel-lgtm for development, or a remote OTLP endpoint:
TELEMETRY_OTLP_ENDPOINT=https://your-collector.example.com
TELEMETRY_OTLP_TOKEN=<bearer-token> # sent as Authorization: Bearer …
What the dashboard shows
Point Telemetry UI at the same backend and three surfaces light up.
Analytics (Monitoring → Analytics)
- Overview — page views, unique visitors, views-per-visit, bounce rate (single-page-view sessions) and average engagement time, above a page-views trend chart (with deploy annotations).
- Top pages — most-viewed pages with distinct visitors each.
- Sources & audience — referrers, and (with geo / UA parsing on) countries and devices.
Frontend (Monitoring → Frontend)
- Page performance — real-user navigation timings (loads, avg load, TTFB,
DOM-interactive) from the
document.loadspans, per page. - Failed browser requests — fetch/XHR calls that 5xx'd or errored; each row opens a representative trace, where a same-origin failure continues into the backend span that caused it.
Errors (Activity → Exceptions)
The unified errors list groups frontend and backend errors together by
exception.group (the same fingerprint both sides stamp), so a JS TypeError
and a PHP exception that are the same issue collapse into one row.
Accuracy & scale
The dashboard reads and aggregates these events from Loki/Tempo. That is:
- Exact for low-traffic sites — the query covers every event.
- A bounded recent sample at scale — the UI caps how many events it scans,
and this LGTM promotes each event's attributes to Loki stream labels, so
high-cardinality fields (
session_id,url_path) get expensive fast.
For real volume, analytics wants a columnar store, not the LGTM stack: point the
emitter's analytics stream at a ClickHouse sink (exact uniq/HLL, funnels,
long retention) — the same dashboard cards read it. LGTM stays perfect for
low-traffic sites and for validating the pipeline.