Files
Primora/apps/backend/db/queries/telemetry.sql
T
Tomas Dvorak e25eb92339 feat: phase 4 integrations — connectors, rybbit analytics, webhooks
- core.integrations (migration 00005) with AES-256-GCM-encrypted
  credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback);
  has_credentials serialization only — secrets never leave the server
- /projects/:id/integrations list/create/delete/test; health check hits
  the stored base URL with a 5s timeout and records status
- Rybbit connector: GET …/integrations/:id/analytics proxies
  /api/organizations + /api/sites/* server-side into a normalized shape
- core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and
  POST …/deploy-markers. Events: issue.created (new fingerprint on
  ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256
  over the raw body; in-process dispatcher, 3 attempts, quadratic
  backoff, 10s timeout, pending-scan on boot
- HTTPS enforced for public targets; HTTP allowed for loopback/private/
  self-hosted sinks
- Integrations page (connectors + webhooks tabs, inline analytics,
  delivery log, secret-shown-once), demo-mode stubs
- 12 new primora_* MCP tools
- workspace 0.4.0

Authored By: TDvorak <[email protected]>
2026-09-16 18:47:31 +02:00

109 lines
4.2 KiB
SQL

-- name: UpsertComponent :one
INSERT INTO core.components (project_id, name, kind)
VALUES ($1, $2, $3)
ON CONFLICT (project_id, name) DO UPDATE SET name = EXCLUDED.name
RETURNING *;
-- name: GetComponentByName :one
SELECT * FROM core.components
WHERE project_id = $1 AND name = $2;
-- name: ListComponents :many
SELECT * FROM core.components
WHERE project_id = $1
ORDER BY name;
-- name: DeleteComponent :exec
DELETE FROM core.components
WHERE id = $1 AND project_id = $2;
-- name: HasFingerprintSeen :one
SELECT EXISTS(
SELECT 1 FROM core.events
WHERE project_id = $1 AND fingerprint = $2 AND type = 'error'
) AS seen;
-- name: InsertEvent :one
INSERT INTO core.events (project_id, component_id, type, severity, message, payload, fingerprint, ts)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: ListEvents :many
SELECT e.*, co.name AS component_name
FROM core.events e
LEFT JOIN core.components co ON co.id = e.component_id
WHERE e.project_id = $1
AND (sqlc.narg('type')::text IS NULL OR e.type = sqlc.narg('type'))
AND (sqlc.narg('component')::text IS NULL OR co.name = sqlc.narg('component'))
AND (sqlc.narg('fingerprint')::text IS NULL OR e.fingerprint = sqlc.narg('fingerprint'))
AND (sqlc.narg('before')::bigint IS NULL OR e.id < sqlc.narg('before'))
ORDER BY e.id DESC
LIMIT sqlc.arg('limit');
-- name: ListErrorGroups :many
SELECT e.fingerprint,
(array_agg(e.message ORDER BY e.ts DESC))[1]::text AS message,
coalesce((array_agg(e.component_id::text ORDER BY e.ts DESC))[1], '')::text AS component_id,
coalesce((array_agg(co.name ORDER BY e.ts DESC))[1], '')::text AS component_name,
count(*) AS count,
min(e.ts)::timestamptz AS first_seen,
max(e.ts)::timestamptz AS last_seen,
(array_agg(e.severity ORDER BY e.ts DESC))[1]::text AS severity
FROM core.events e
LEFT JOIN core.components co ON co.id = e.component_id
WHERE e.project_id = $1 AND e.type = 'error' AND e.fingerprint <> ''
AND e.ts > now() - (sqlc.narg('days')::text || ' days')::interval
GROUP BY e.fingerprint
ORDER BY max(e.ts) DESC
LIMIT 200;
-- name: EventTypeSeries :many
SELECT to_timestamp(floor(extract(epoch FROM e.ts) / sqlc.arg('bucket_sec')::bigint) * sqlc.arg('bucket_sec')::bigint)::timestamptz AS bucket,
e.type,
count(*) AS count
FROM core.events e
WHERE e.project_id = $1 AND e.ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint)
GROUP BY bucket, e.type
ORDER BY bucket;
-- name: ComponentHealthRows :many
SELECT co.id, co.name, co.kind, co.meta,
max(e.ts)::timestamptz AS last_seen_at,
coalesce(((array_agg(e.payload->>'status' ORDER BY e.ts DESC)
FILTER (WHERE e.type = 'heartbeat'))[1]), '')::text AS last_status,
count(*) FILTER (WHERE e.type = 'error' AND e.ts > now() - interval '24 hours') AS errors_24h,
count(*) FILTER (WHERE e.ts > now() - interval '24 hours') AS events_24h
FROM core.components co
LEFT JOIN core.events e ON e.component_id = co.id
WHERE co.project_id = $1
GROUP BY co.id
ORDER BY co.name;
-- name: EventTotals :one
SELECT count(*) FILTER (WHERE type = 'error') AS errors,
count(*) AS events,
count(*) FILTER (WHERE type = 'metric') AS metrics
FROM core.events
WHERE project_id = $1 AND ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint);
-- name: MetricNames :many
SELECT DISTINCT (payload->>'name')::text AS name
FROM core.events
WHERE project_id = $1 AND type = 'metric' AND payload ? 'name'
ORDER BY 1
LIMIT 50;
-- name: MetricSeries :many
SELECT to_timestamp(floor(extract(epoch FROM ts) / sqlc.arg('bucket_sec')::bigint) * sqlc.arg('bucket_sec')::bigint)::timestamptz AS bucket,
avg((payload->>'value')::float8)::float8 AS avg,
percentile_cont(0.5) WITHIN GROUP (ORDER BY (payload->>'value')::float8)::float8 AS p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY (payload->>'value')::float8)::float8 AS p95,
max((payload->>'value')::float8)::float8 AS max,
count(*) AS count
FROM core.events
WHERE project_id = $1 AND type = 'metric' AND payload->>'name' = sqlc.arg('name')::text
AND ts > now() - make_interval(secs => sqlc.arg('window_sec')::bigint)
AND jsonb_typeof(payload->'value') = 'number'
GROUP BY bucket
ORDER BY bucket;