Files
cairnobs/api
jcoffey-dev 5e8b3d8edd Add a real charting layer and a heatmap panel type
Five chart types on ECharts (modular imports, not the full bundle):
TimeSeriesChart (multi-series, legend toggle), BarChart (incl.
stacked), SingleStat (big number + sparkline + trend), Heatmap, TopN.
Shared interactions: tooltips, dataZoom feeding the global time-range
picker, click-to-drill-into-query (drilldown.ts strips a panel's query
to its pre-stats filter and appends the clicked series/x-value as a
new filter term -- no backend change needed).

pivot.ts reshapes the query language's existing {columns, rows} tabular
output into per-series chart data client-side -- `stats count by
service, timestamp` already returns "long" rows, so multi-series
support needed zero query-language changes. theme.ts reads real
computed CSS custom properties so charts render in the active theme's
actual colors, with an SSR_FALLBACK for adapter-static's prerender pass
where `document` doesn't exist.

heatmap is the one narrow, justified backend change: a new VizType
needed to feed a new visualization, not a new query capability. Three
places had to change together, not two -- api/dashboards/types.go's
validator, web/src/lib/api.ts's union (previous commit), and the
dashboard_panels table's viz_type CHECK constraint
(migrations/0035_add_heatmap_viz_type.sql), which mirrors the Go
validator and doesn't update itself.

/dev/charts (unlisted, dev-only) is a synthetic fixture/perf-test route:
confirmed 50ms first-two-frames render time on a production build
against a 30,006-row/6-series stress case, and a 211,975-byte gzipped
chart chunk -- both real measurements behind the ECharts-over-
Observable-Plot-or-D3 choice, not estimates.
2026-08-16 12:35:40 -07:00
..
2026-08-13 17:29:38 -07:00
2026-08-13 17:29:38 -07:00

api

Sentry's query API: a single POST /query endpoint accepting either the pipe syntax or raw SQL, compiled and routed across ClickHouse and Tantivy by internal/querylang. Replaces Phase 0/1's two separate placeholder endpoints (raw-SQL-only /query, free-text-only /search) — see /docs/query-language-design.md for the grammar, IR, and routing design, and /docs/query-language-reference.md for the user-facing syntax.

Why plain REST, not gRPC + REST gateway

CLAUDE.md pins the control plane to "Go, gRPC + REST gateway." This service is plain net/http instead — a deliberate simplification, not a change to the pinned stack. Wiring up a .proto service, google.api.http annotations, and protoc-gen-grpc-gateway codegen for one endpoint doesn't buy much at this size. api does speak gRPC internally — to /search — this simplification is about the public-facing surface only.

Endpoint

POST /query — body {"query": "...", "language": ""}, response {"columns": [...], "rows": [[...], ...]} or {"error": "..."}.

  • query is either pipe syntax (service=api | where status>=500 | stats count by host) or raw SQL (SELECT ...). Auto-detected by whether the query starts with SELECT (case-insensitive).
  • language optionally overrides detection: "sql" or "spl". Exists for the rare case a pipe query legitimately starts with the literal word "select" as a bare search term.
  • Both syntaxes compile to the same querylang/ir.Plan and execute through the same code path — see internal/querylang/executor for the four routing cases (pure ClickHouse; Tantivy prefilter + ClickHouse rows; Tantivy prefilter + ClickHouse aggregation; raw SQL passthrough).

GET /healthz — for docker-compose/k8s liveness checks.

No auth. Not scoped yet — don't expose this beyond a trusted dev/homelab network.

Configuration

Environment variables (see internal/config/config.go):

Var Default Purpose
HTTP_LISTEN_ADDR :8080
CLICKHOUSE_ADDR localhost:9000 Native protocol port
CLICKHOUSE_DATABASE / _USERNAME / _PASSWORD sentry / default / ``
SEARCH_GRPC_ADDR localhost:50052 Must match /search's GRPC_LISTEN_ADDR
QUERY_TIMEOUT_SECONDS 30 Per-request timeout
CORS_ALLOWED_ORIGIN * Wide open by default since there's no auth yet; tighten together

searchclient.Dial connects to /search over plain TCP, no TLS — same trust boundary as api's existing plain-TCP connection to ClickHouse. mTLS in this project is specifically the agent↔ingest edge boundary, not every internal hop.

Building & testing

go build ./...
go vet ./...
go test ./...
# from the repo root, not api/
docker build -f api/Dockerfile -t sentry-api .

Testing notes

internal/queryapi's HTTP handler depends on ClickHouse and /search only through the narrow interfaces querylang/executor defines (SQLRunner, SearchClient), so routing, compilation, JSON encoding, and error-status mapping are all unit-tested against fakes — no live ClickHouse or /search instance needed, and the real lexer/parser/ planner run unmocked in these tests, only the backends are faked. See internal/querylang's own package docs for how compilation and execution are tested independently of each other. executor.ChRunner (the reflection-based row scanning against ClickHouse's driver.Rows) and internal/searchclient's actual gRPC dial are not unit-tested — the former because faking driver.Rows fully would be significant test-only scaffolding the driver's own docs say isn't meant to be implemented by adopters; the latter because it's a thin wrapper with nothing but wiring to test. Both are exercised end-to-end via the docker-compose flow in /docs/phase-2-runbook.md.