feat: phase 3 observability — IMS telemetry merge

Port IMS telemetry into Primora conventions:

- core.components/core.events (migration 00004 + sqlc) scoped to projects
- POST /api/v1/ingest — single or <=500-event batches via project prm_
  keys (X-Primora-Key alias), auto-registering components, permissive CORS
- Issues: fingerprint-grouped errors with count/severity/first+last seen
- Reads: events w/ filters + before-cursor, components, stats (1h/24h/7d/30d
  bucketed series + component health + totals + metric names), metric
  series (avg/p50/p95/max)
- GET .../telemetry/stream — project-scoped SSE through an in-process hub
  (buffered subs, drop-on-slow, 25s keepalives); ?api_key=/?token= auth
  for EventSource
- Telemetry page: health/issues/events/metrics/integrate tabs, live badge,
  390px-clean; demo stubs
- @primora/client SDK (createPrimoraClient: capture*/heartbeat/installAuto,
  queued keepalive flushes)
- MCP: primora_list_issues/list_events/telemetry_stats/metric_series
- IMS README deprecation note pointing here

Also fixes workspace pins to 0.3.0 — npm ci in the frontend image was
resolving the unpublished 0.2.0 spec from the registry.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-16 16:21:03 +02:00
1 parent 6507eee606
commit 8cfddf9184
60 files changed
+3694 -30

No files matched your search

@@ -0,0 +1,44 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ComponentHealth = {
properties: {
id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
name: {
type: 'string',
isRequired: true,
},
kind: {
type: 'string',
isRequired: true,
},
last_seen_at: {
type: 'string',
isNullable: true,
format: 'date-time',
},
last_status: {
type: 'string',
},
errors_24h: {
type: 'number',
isRequired: true,
},
events_24h: {
type: 'number',
isRequired: true,
},
meta: {
type: 'dictionary',
contains: {
properties: {
},
},
},
},
} as const;
@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ComponentID = {
type: 'string',
format: 'uuid',
} as const;
@@ -0,0 +1,20 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $EventSeriesBucket = {
properties: {
ts: {
type: 'string',
isRequired: true,
format: 'date-time',
},
counts: {
type: 'dictionary',
contains: {
type: 'number',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,40 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $IngestEvent = {
properties: {
type: {
type: 'Enum',
isRequired: true,
},
component: {
type: 'string',
description: `Auto-registered under the project on first use`,
},
kind: {
type: 'Enum',
},
severity: {
type: 'string',
},
message: {
type: 'string',
},
payload: {
type: 'dictionary',
contains: {
properties: {
},
},
},
fingerprint: {
type: 'string',
description: `Error grouping key; derived from message when omitted`,
},
ts: {
type: 'string',
format: 'date-time',
},
},
} as const;
@@ -0,0 +1,20 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $IngestRequest = {
type: 'one-of',
contains: [{
type: 'IngestEvent',
}, {
properties: {
events: {
type: 'array',
contains: {
type: 'IngestEvent',
},
isRequired: true,
},
},
}],
} as const;
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $IngestResponse = {
properties: {
accepted: {
type: 'number',
isRequired: true,
},
},
} as const;
@@ -0,0 +1,33 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $MetricPoint = {
properties: {
ts: {
type: 'string',
isRequired: true,
format: 'date-time',
},
avg: {
type: 'number',
isNullable: true,
},
p50: {
type: 'number',
isNullable: true,
},
p95: {
type: 'number',
isNullable: true,
},
max: {
type: 'number',
isNullable: true,
},
count: {
type: 'number',
isRequired: true,
},
},
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $MetricSeriesResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'MetricPoint',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,44 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryComponent = {
properties: {
id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
name: {
type: 'string',
isRequired: true,
},
kind: {
type: 'string',
isRequired: true,
},
last_seen_at: {
type: 'string',
isNullable: true,
format: 'date-time',
},
last_status: {
type: 'string',
},
errors_24h: {
type: 'number',
isRequired: true,
},
events_24h: {
type: 'number',
isRequired: true,
},
meta: {
type: 'dictionary',
contains: {
properties: {
},
},
},
},
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryComponentListResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'TelemetryComponent',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,52 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryEvent = {
properties: {
id: {
type: 'number',
isRequired: true,
},
project_id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
component_id: {
type: 'string',
isNullable: true,
format: 'uuid',
},
component_name: {
type: 'string',
},
type: {
type: 'string',
isRequired: true,
},
severity: {
type: 'string',
isRequired: true,
},
message: {
type: 'string',
isRequired: true,
},
payload: {
type: 'dictionary',
contains: {
properties: {
},
},
},
fingerprint: {
type: 'string',
},
ts: {
type: 'string',
isRequired: true,
format: 'date-time',
},
},
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryEventListResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'TelemetryEvent',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,42 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryIssue = {
properties: {
fingerprint: {
type: 'string',
isRequired: true,
},
message: {
type: 'string',
isRequired: true,
},
component_id: {
type: 'string',
isNullable: true,
format: 'uuid',
},
component_name: {
type: 'string',
},
count: {
type: 'number',
isRequired: true,
},
first_seen: {
type: 'string',
isRequired: true,
format: 'date-time',
},
last_seen: {
type: 'string',
isRequired: true,
format: 'date-time',
},
severity: {
type: 'string',
isRequired: true,
},
},
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryIssueListResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'TelemetryIssue',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,49 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $TelemetryStats = {
properties: {
window: {
type: 'number',
isRequired: true,
},
bucket_sec: {
type: 'number',
isRequired: true,
},
series: {
type: 'array',
contains: {
type: 'EventSeriesBucket',
},
isRequired: true,
},
components: {
type: 'array',
contains: {
type: 'ComponentHealth',
},
isRequired: true,
},
errors: {
type: 'number',
isRequired: true,
},
events: {
type: 'number',
isRequired: true,
},
metrics: {
type: 'number',
isRequired: true,
},
metric_names: {
type: 'array',
contains: {
type: 'string',
},
isRequired: true,
},
},
} as const;