Unified Observe Page
Observe is a project-level surface that merges four kinds of event — requests, traces, errors, and revenue — into a single time-ordered stream. Use it when you want one place to ask "what happened to this project, in order" without hopping between the Requests, Traces, Errors, and Revenue views.
It lives in the web console at /projects/:slug/observe and is backed by the temps-observability crate, which exposes two authenticated HTTP endpoints. Runtime logs are intentionally excluded — they stay on the dedicated Logs page because their volume would dominate the merged business-signal timeline.
Overview
What it is
- One merged page of requests (
proxy_logs), traces (otel_spans), errors (error_events), and revenue (revenue_events) - K-way merged by timestamp, newest first
- A cockpit header with one clickable sparkline per kind
- A side panel that renders from the row payload alone — no follow-up fetch in the common case
- Filter state stored in the URL so a view is shareable
When to use it
- Triage after a deploy: see the 500s, the errors they produced, and the revenue events around them in one timeline
- Correlate a request with the trace, error group, and revenue event it touched
- Hand a teammate a link to exactly what you're looking at — the filters live in the URL
Observe is a read surface over data the platform already collects. The temps-observability crate only reads the source tables via shared entities; it does not change how requests, traces, errors, or revenue are ingested. See Error Tracking, Analytics, and Monitoring for how that data gets there.
When to use
Reach for Observe when you want to triage a deployment incident and correlate errors, requests, traces, and revenue events in one timeline — for example, to see the 500s, the error groups they produced, and the revenue events around them without switching pages.
Skip it if you only care about a single signal type. Use the dedicated Error Tracking, OpenTelemetry (traces), or Analytics pages instead — each gives that one kind more room and more kind-specific filters than the merged stream does.
How it works
The merge service runs one query per enabled kind against its source table, maps each result into the unified ObservabilityEvent wire type (truncating heavy fields server-side), then k-way merges the per-kind streams by timestamp descending and trims to a single page.
- Requests come from
proxy_logs(Sea-ORM). - Errors come from
error_events(Sea-ORM). - Revenue comes from
revenue_events(Sea-ORM). - Traces come from the
otel_spansTimescaleDB hypertable, queried via raw SQL (FromQueryResult).
Each per-kind query carries its own LIMIT equal to the page size, so no single kind can starve the others before the merge. The merge itself is a deterministic linear scan: ties on timestamp fall back to a stable (kind, id) ordering so repeated requests return rows in the same order.
The per-kind queries are awaited sequentially, not run in parallel. The service issues each enabled kind's query in turn and merges the collected streams once they have all returned.
Event kinds
The cockpit header renders one sparkline card per kind over the selected time range. Clicking a card toggles that kind on or off, and the filter is reflected in the URL.
| Kind | Source table | Accent color | Default |
|---|---|---|---|
request | proxy_logs | sky | On |
span (Traces) | otel_spans | violet | Off |
error | error_events | rose | On |
revenue | revenue_events | emerald | On |
Traces are opt-in because otel_spans is high-volume. The web console defaults to request, error, and revenue only — click the Traces cockpit card to enable them per session. There is no log kind: runtime logs are not part of the merged union.
List endpoint
Endpoint
- Name
GET /api/projects/{project_id}/observe/events- Type
- endpoint
- Description
Returns a merged, time-ordered page of
ObservabilityEventrows. Requires theLogsReadpermission.
Endpoint
GET /api/projects/{project_id}/observe/events
Authorization: Bearer <temps-api-key>
temps-observability is a Temps plugin, and plugin routes are served under the /api prefix. The handler path is declared as /projects/{project_id}/observe/events, so the served path is GET /api/projects/{project_id}/observe/events.
Query parameters
- Name
kinds- Type
- string
- Description
Comma-separated list of
request,span,error,revenue. Empty or missing returns all kinds. An unknown token is rejected with400.
- Name
from- Type
- string
- Description
Inclusive lower bound on event timestamp (ISO 8601,
Zsuffix).
- Name
to- Type
- string
- Description
Inclusive upper bound on event timestamp (ISO 8601,
Zsuffix). Afromlater thantois rejected with400.
- Name
deployment_id- Type
- integer
- Description
Filter to a single deployment.
- Name
environment_id- Type
- integer
- Description
Filter to a single environment.
- Name
search- Type
- string
- Description
Free-text substring matched against per-kind summary fields (request path / error class / revenue event type).
- Name
hide_bots- Type
- boolean
- Description
When
true, exclude bot/crawler request rows. Whenfalse, only include bot rows. Omitted means include everything. Only affects therequestkind.
- Name
limit- Type
- integer
- Description
Page size. Defaults to
50, capped server-side at200(and floored at1).
Response
EventsResponse
{
"events": [
{ "type": "request", "id": 9921, "ts": "2026-05-02T14:02:11Z", "method": "GET", "host": "app.example.com", "path": "/checkout", "status": 500, "...": "..." },
{ "type": "error", "id": 5512, "ts": "2026-05-02T14:02:11Z", "error_group_id": 41, "error_class": "TypeError", "...": "..." },
{ "type": "revenue", "id": 88, "ts": "2026-05-02T14:01:55Z", "provider": "stripe", "event_type": "invoice.paid", "...": "..." }
],
"applied_kinds": ["request", "error", "revenue"]
}
applied_kinds echoes the kind set the server actually resolved, which is useful when you pass an empty kinds= and want to know what you got back.
Full-event endpoint
Each list row carries truncated previews of heavy fields plus a *_truncated flag (see Server-side truncation). When you need the un-truncated form of a single event, fetch it by (kind, id).
- Name
GET /api/projects/{project_id}/observe/events/{kind}/{event_id}/full- Type
- endpoint
- Description
Returns the un-truncated form of one event. Requires the
LogsReadpermission.{kind}is one ofrequest,span,error,revenue;{event_id}is that kind's primary key.
Full event
GET /api/projects/{project_id}/observe/events/error/5512/full
Authorization: Bearer <temps-api-key>
This endpoint exists and is exposed in the generated SDK, but the current web UI does not call it. Instead, the side panel's "Show full" actions link out to the dedicated error-group / trace pages. The endpoint is available for API consumers that want the raw, un-truncated row directly.
Wire shape
Every row in the stream is a discriminated union: it serializes with a type field whose value is one of request, span, error, or revenue. The client switches on type to pick a renderer.
Each row also carries everything the side panel needs — method, path, status, error class, trace/correlation IDs, revenue provider and amount, and so on — so opening detail does not require a second request in the common case.
Discriminated rows
// request row
{ "type": "request", "id": 9921, "ts": "2026-05-02T14:02:11Z", "method": "GET", "host": "app.example.com", "path": "/checkout", "status": 500, "latency_ms": 812, "trace_id": "4bf9…4736", "error_group_id": 41, "headers_truncated": true }
// span row (traces)
{ "type": "span", "id": "…", "ts": "…", "trace_id": "4bf9…4736", "span_id": "…", "service": "web", "operation": "GET /checkout", "duration_ms": 803.4, "attributes_truncated": true }
// error row
{ "type": "error", "id": 5512, "ts": "…", "error_group_id": 41, "fingerprint": "…", "error_class": "TypeError", "message": "…", "stacktrace_truncated": true }
// revenue row
{ "type": "revenue", "id": 88, "ts": "…", "provider": "stripe", "event_type": "invoice.paid", "amount_minor": 4200, "currency": "usd" }
Server-side truncation
Heavy fields are trimmed before they leave the server so a 200-row page stays small. Each trimmed field has a matching *_truncated boolean so the client knows whether more data exists.
| Field | Truncated to | Flag |
|---|---|---|
| Error stack frames | First 5 frames (order preserved) | stacktrace_truncated |
| Span attributes | First 20 keys, alphabetized | attributes_truncated |
| Request / response headers | A fixed 10-key whitelist | headers_truncated |
The header whitelist is exactly: host, user-agent, referer, referrer, content-type, content-length, accept, accept-encoding, x-forwarded-for, cache-control. Any other header is dropped from the preview (and headers_truncated is set), but remains available via the full-event endpoint.
Worked example
Triage a spike after a deploy: pull every error and 500-prone request for project 7 over a two-hour window, hiding bots, on a 100-row page.
List errors + requests for a window
curl "https://your-temps-instance.com/api/projects/7/observe/events?kinds=request,error&from=2026-05-02T13:00:00Z&to=2026-05-02T15:00:00Z&hide_bots=true&limit=100" \
-H "Authorization: Bearer your-temps-api-key"
Spot an error you want the full stack trace for (its row has "stacktrace_truncated": true), then fetch the un-truncated row by kind and id:
Fetch one full error
curl "https://your-temps-instance.com/api/projects/7/observe/events/error/5512/full" \
-H "Authorization: Bearer your-temps-api-key"
In the web console, the same triage is a shareable URL — for example:
Shareable console URL
/projects/my-app/observe?kinds=request,error&time_range=1h&hide_bots=false
Correlation columns
A migration (m20260502_000001_add_observe_correlation) adds nullable cross-source correlation columns and lookup indexes so the view can jump from any event to its peers in the same trace. All columns are nullable — old rows simply render without correlation links.
| Table | Columns added |
|---|---|
proxy_logs | trace_id (text), error_group_id (integer) |
revenue_events | deployment_id (integer), environment_id (integer), trace_id (text) |
error_events | trace_id_indexed (text) |
Supporting indexes: idx_proxy_logs_project_trace, idx_proxy_logs_error_group, idx_error_events_project_trace, and idx_revenue_events_project_occurred. The migration uses ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS throughout so it is safe to re-run.
Notes & gotchas
/apiprefix. Handler paths are declared without it; the plugin listener serves them under/api. HitGET /api/projects/{project_id}/observe/events, not/projects/....LogsReadpermission. Both endpoints require it. A key without it is rejected by the permission guard.- Traces are opt-in. Because
otel_spansis high-volume, traces are off by default; the default kind set isrequest,error,revenue. Toggle them on via the cockpit Traces card. - No runtime logs. There is no
logkind in the union. Runtime stdout/stderr stays on the Logs page. - Limit is clamped. A
limitabove200is capped to200; below1it is raised to1. hide_botsonly touches requests. Other kinds do not carry a bot flag, so the filter is a no-op for them.
Related
- Logs — the dedicated runtime-log surface (deliberately separate from Observe)
- Error Tracking — how errors are grouped and ingested
- OpenTelemetry — how traces (
otel_spans) are ingested and queried - Analytics — request and event data
- Monitoring — uptime and resource monitoring