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

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_spans TimescaleDB 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.


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.

KindSource tableAccent colorDefault
requestproxy_logsskyOn
span (Traces)otel_spansvioletOff
errorerror_eventsroseOn
revenuerevenue_eventsemeraldOn

List endpoint

Endpoint

  • Name
    GET /api/projects/{project_id}/observe/events
    Type
    endpoint
    Description

    Returns a merged, time-ordered page of ObservabilityEvent rows. Requires the LogsRead permission.

Endpoint

GET /api/projects/{project_id}/observe/events
Authorization: Bearer <temps-api-key>

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 with 400.

  • Name
    from
    Type
    string
    Description

    Inclusive lower bound on event timestamp (ISO 8601, Z suffix).

  • Name
    to
    Type
    string
    Description

    Inclusive upper bound on event timestamp (ISO 8601, Z suffix). A from later than to is rejected with 400.

  • 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. When false, only include bot rows. Omitted means include everything. Only affects the request kind.

  • Name
    limit
    Type
    integer
    Description

    Page size. Defaults to 50, capped server-side at 200 (and floored at 1).

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 LogsRead permission. {kind} is one of request, 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>

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.

FieldTruncated toFlag
Error stack framesFirst 5 frames (order preserved)stacktrace_truncated
Span attributesFirst 20 keys, alphabetizedattributes_truncated
Request / response headersA fixed 10-key whitelistheaders_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.

TableColumns added
proxy_logstrace_id (text), error_group_id (integer)
revenue_eventsdeployment_id (integer), environment_id (integer), trace_id (text)
error_eventstrace_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

  • /api prefix. Handler paths are declared without it; the plugin listener serves them under /api. Hit GET /api/projects/{project_id}/observe/events, not /projects/....
  • LogsRead permission. Both endpoints require it. A key without it is rejected by the permission guard.
  • Traces are opt-in. Because otel_spans is high-volume, traces are off by default; the default kind set is request, error, revenue. Toggle them on via the cockpit Traces card.
  • No runtime logs. There is no log kind in the union. Runtime stdout/stderr stays on the Logs page.
  • Limit is clamped. A limit above 200 is capped to 200; below 1 it is raised to 1.
  • hide_bots only touches requests. Other kinds do not carry a bot flag, so the filter is a no-op for them.
  • 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

Was this page helpful?