Skip to content

API Reference

The JSON shape of the two types that cross a process boundary: DeployEvent and PerformanceRegression.

Overview

These are the two types every webhook source normalises into and every alert is built from. Both are plain Go DTOs in pkg/apis/v1alpha1 — see Architecture Overview's note on the difference between these and the real Kubernetes CRDs in api/v1alpha1.

DeployEvent

{
  "id": "argocd-my-app-1234567890",
  "source": "prod-argocd",
  "app": "my-app",
  "cluster": "prod-cluster",
  "namespace": "production",
  "revision": "abc123def456",
  "imageTag": "my-app:v42",
  "timestamp": "2026-08-11T12:00:00Z"
}

Produced by the Ingester from any of the four webhook sources — see Deploy Sources & Webhooks for how each source populates cluster.

PerformanceRegression

{
  "name": "argocd-my-app-1234567890-q8675309",
  "namespace": "production",
  "deployEventId": "argocd-my-app-1234567890",
  "queryId": 8675309,
  "queryText": "SELECT * FROM orders WHERE user_id = $1",
  "status": "Detected",
  "confidenceScore": 0.98,
  "meanLatencyBefore": 4.2,
  "meanLatencyAfter": 13.7,
  "latencyChangeFactor": 3.26,
  "externalCauseSuspected": false,
  "planDiffSummary": "root node changed from Index Scan to Seq Scan",
  "createdAt": "2026-08-11T12:35:00Z"
}

Produced by the Correlation Engine for every query analysed against a DeployEvent — see Detection Algorithm for what status, confidenceScore, and the latency fields mean, and how Detected is decided. status is one of Detected, NoRegression, or InsufficientData. planDiffSummary is only populated when plan-diff correlation is enabled (--capture-plans / spec.capturePlans) — see Detection Algorithm: Plan-diff correlation.

Dashboard API

internal/dashboardapi serves a read-only HTTP API for the observability dashboard. It is enabled with --dashboard-addr on cmd/operator and cmd/manager (see Configuration Reference) and is a pure read layer: every field returned already exists on the PerformanceRegression/PostgresWatch CRDs (api/v1alpha1) or in internal/storage/internal/collector/internal/ingester, with no new computed metrics. Fields the current detection pipeline does not compute — t-statistic, Cohen's d, confidence intervals, e-divisive permutation counts, cache-hit ratio, rows-examined, lock-wait time, p95/p99 latency — are not part of this API.

Each route returns 501 Not Implemented if the process serving it wasn't wired with the dependency that route needs. /api/v1/regressions and /api/v1/watches list Kubernetes objects and need a client.Client — only cmd/manager has one, so they always 501 on cmd/operator. /api/v1/deploys and /api/v1/queries need query-sample/deploy-event history, which only cmd/operator builds (via its Collector/deploy-event Store, or the postgres state backend when --state-backend=postgres) — they always 501 on cmd/manager.

Every route responds with permissive CORS headers (Access-Control-Allow-Origin: *, GET and OPTIONS only), so a browser-based dashboard served from a different origin can call this API directly without a proxy. This is safe precisely because the API is read-only and never combines the open origin with credentialed/cookie-based access.

queryId is a JSON string, not a number, on every route below (/api/v1/regressions, /api/v1/queries, and /api/v1/queries/{queryId}/samples) — pg_stat_statements queryids are arbitrary int64 values that routinely exceed JavaScript's Number.MAX_SAFE_INTEGER (2^53-1), so a bare JSON number would get silently rounded by JSON.parse in any JS consumer. Treat it as an opaque string identifier: pass it back verbatim (e.g. into /api/v1/queries/{queryId}/samples), never parse it into a number.

GET /api/v1/regressions?namespace=&status=

Lists PerformanceRegression objects (api/v1alpha1/performanceregression_types.go) via a client.Client, optionally filtered by namespace and status (Detected, NoRegression, InsufficientData). Each item:

{
  "name": "argocd-my-app-1234567890-q8675309",
  "namespace": "production",
  "clusterName": "prod-cluster",
  "queryId": "8675309",
  "queryText": "SELECT * FROM orders WHERE user_id = $1",
  "deployEventId": "argocd-my-app-1234567890",
  "status": "Detected",
  "triggerType": "deploy",
  "confidenceScore": "0.98",
  "meanLatencyBeforeMs": "4.2",
  "meanLatencyAfterMs": "13.7",
  "latencyChangeFactor": "3.26",
  "externalCauseSuspected": false,
  "planDiffSummary": "root node changed from Index Scan to Seq Scan",
  "autoAbortTriggered": false,
  "detectedAt": "2026-08-11T12:35:00Z"
}

deployEventId is omitted for a periodic-triggered regression (no deploy involved); when present, cross-reference it against GET /api/v1/deploys (below) to show the real deploy that triggered the regression — app, image tag, revision, timestamp.

GET /api/v1/deploys?since=RFC3339&until=RFC3339

Returns DeployEvent (above), unmodified. Backed by storage.EventStore.EventsInRange when --state-backend=postgres, or otherwise directly by the operator's own in-process deploy-event history (internal/ingester.Store) — either way this works with cmd/operator's default configuration, not only when postgres persistence is opted into. Defaults to the last 7 days when since/until are omitted.

GET /api/v1/queries?windowMinutes=60

Returns, per tracked queryId, an aggregate over samples recorded in the last windowMinutes (default 60). Backed by storage.SampleStore when --state-backend=postgres, or otherwise directly by the operator's own in-process query-sample history (internal/collector.Collector) — either way this works with cmd/operator's default configuration:

{
  "queryId": "8675309",
  "queryText": "SELECT * FROM orders WHERE user_id = $1",
  "calls": 4213,
  "meanExecMs": 4.2,
  "sampleCount": 12
}

calls and meanExecMs mirror exactly what regression_radar.query_samples stores (calls, mean_exec_time_ms); there is no p95/p99 or cache-hit-ratio field, since that data isn't collected today.

GET /api/v1/queries/{queryId}/samples?since=RFC3339&until=RFC3339

Returns the raw, per-scrape samples for one queryId in [since, until] — unlike /api/v1/queries above, which collapses a whole window into one aggregate, this is one entry per pg_stat_statements scrape, meant for plotting an actual time series (e.g. the real before/after curve around a specific regression's detectedAt) rather than a single before/after number. Defaults to the last 60 minutes when since/until are omitted, same as /api/v1/queries. Backed by the same SampleReader as /api/v1/queries, so it 501s under the same conditions.

[
  { "recordedAt": "2026-08-11T12:30:00Z", "calls": 4201, "meanExecMs": 4.1 },
  { "recordedAt": "2026-08-11T12:35:00Z", "calls": 4213, "meanExecMs": 13.7 }
]

GET /api/v1/watches

Lists PostgresWatch objects (api/v1alpha1/postgreswatch_types.go) as-is, with no transformation.

See also