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

+436
View File
@@ -13,6 +13,7 @@ tags:
- name: Storage
- name: Collections
- name: Databases
- name: Telemetry
paths:
/health/liveness:
get:
@@ -1091,6 +1092,200 @@ paths:
$ref: "#/components/responses/ErrorResponse"
"502":
$ref: "#/components/responses/ErrorResponse"
/ingest:
post:
tags: [Telemetry]
operationId: ingestTelemetry
summary: Ingest one telemetry event or a batch of up to 500
security:
- apiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/IngestRequest"
responses:
"202":
description: Accepted event count
content:
application/json:
schema:
$ref: "#/components/schemas/IngestResponse"
"400":
$ref: "#/components/responses/ErrorResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
"413":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/events:
get:
tags: [Telemetry]
operationId: listTelemetryEvents
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: type
in: query
schema:
type: string
enum: [error, metric, log, heartbeat, event]
- name: component
in: query
schema:
type: string
- name: fingerprint
in: query
schema:
type: string
- name: before
in: query
schema:
type: integer
- name: limit
in: query
schema:
type: integer
responses:
"200":
description: Telemetry events, newest first
content:
application/json:
schema:
$ref: "#/components/schemas/TelemetryEventListResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/issues:
get:
tags: [Telemetry]
operationId: listTelemetryIssues
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: days
in: query
schema:
type: integer
responses:
"200":
description: Error groups by fingerprint
content:
application/json:
schema:
$ref: "#/components/schemas/TelemetryIssueListResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/components:
get:
tags: [Telemetry]
operationId: listTelemetryComponents
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
responses:
"200":
description: Registered telemetry components
content:
application/json:
schema:
$ref: "#/components/schemas/TelemetryComponentListResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/components/{componentID}:
delete:
tags: [Telemetry]
operationId: deleteTelemetryComponent
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- $ref: "#/components/parameters/ComponentID"
responses:
"204":
description: Component deleted
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/telemetry/stats:
get:
tags: [Telemetry]
operationId: getTelemetryStats
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: window
in: query
schema:
type: string
enum: [1h, 24h, 7d, 30d]
responses:
"200":
description: Aggregated telemetry statistics
content:
application/json:
schema:
$ref: "#/components/schemas/TelemetryStats"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/telemetry/metrics/series:
get:
tags: [Telemetry]
operationId: getTelemetryMetricSeries
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: name
in: query
required: true
schema:
type: string
- name: window
in: query
schema:
type: string
enum: [1h, 24h, 7d, 30d]
responses:
"200":
description: Aggregated metric series
content:
application/json:
schema:
$ref: "#/components/schemas/MetricSeriesResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/telemetry/stream:
get:
tags: [Telemetry]
operationId: streamTelemetryEvents
summary: Server-sent events stream of live telemetry. EventSource clients pass `?api_key=` or `?token=` since headers cannot be set.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: api_key
in: query
schema:
type: string
- name: token
in: query
schema:
type: string
responses:
"200":
description: text/event-stream of TelemetryEvent messages
"401":
$ref: "#/components/responses/ErrorResponse"
components:
securitySchemes:
bearerAuth:
@@ -1151,6 +1346,13 @@ components:
schema:
type: string
format: uuid
ComponentID:
name: componentID
in: path
required: true
schema:
type: string
format: uuid
UserID:
name: userID
in: path
@@ -2094,3 +2296,237 @@ components:
output:
type: string
required: [output]
IngestEvent:
type: object
properties:
type:
type: string
enum: [error, metric, log, heartbeat, event]
component:
type: string
description: Auto-registered under the project on first use
kind:
type: string
enum: [frontend, backend, database, android, desktop, web, other]
severity:
type: string
message:
type: string
payload:
type: object
additionalProperties: true
fingerprint:
type: string
description: Error grouping key; derived from message when omitted
ts:
type: string
format: date-time
required: [type]
IngestRequest:
oneOf:
- $ref: "#/components/schemas/IngestEvent"
- type: object
properties:
events:
type: array
items:
$ref: "#/components/schemas/IngestEvent"
maxItems: 500
required: [events]
IngestResponse:
type: object
properties:
accepted:
type: integer
required: [accepted]
TelemetryEvent:
type: object
properties:
id:
type: integer
project_id:
type: string
format: uuid
component_id:
type: string
format: uuid
nullable: true
component_name:
type: string
type:
type: string
severity:
type: string
message:
type: string
payload:
type: object
additionalProperties: true
fingerprint:
type: string
ts:
type: string
format: date-time
required: [id, project_id, type, severity, message, ts]
TelemetryEventListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/TelemetryEvent"
required: [items]
TelemetryIssue:
type: object
properties:
fingerprint:
type: string
message:
type: string
component_id:
type: string
format: uuid
nullable: true
component_name:
type: string
count:
type: integer
first_seen:
type: string
format: date-time
last_seen:
type: string
format: date-time
severity:
type: string
required: [fingerprint, message, count, first_seen, last_seen, severity]
TelemetryIssueListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/TelemetryIssue"
required: [items]
TelemetryComponent:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
kind:
type: string
last_seen_at:
type: string
format: date-time
nullable: true
last_status:
type: string
errors_24h:
type: integer
events_24h:
type: integer
meta:
type: object
additionalProperties: true
required: [id, name, kind, errors_24h, events_24h]
TelemetryComponentListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/TelemetryComponent"
required: [items]
ComponentHealth:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
kind:
type: string
last_seen_at:
type: string
format: date-time
nullable: true
last_status:
type: string
errors_24h:
type: integer
events_24h:
type: integer
meta:
type: object
additionalProperties: true
required: [id, name, kind, errors_24h, events_24h]
EventSeriesBucket:
type: object
properties:
ts:
type: string
format: date-time
counts:
type: object
additionalProperties:
type: integer
required: [ts, counts]
TelemetryStats:
type: object
properties:
window:
type: integer
bucket_sec:
type: integer
series:
type: array
items:
$ref: "#/components/schemas/EventSeriesBucket"
components:
type: array
items:
$ref: "#/components/schemas/ComponentHealth"
errors:
type: integer
events:
type: integer
metrics:
type: integer
metric_names:
type: array
items:
type: string
required: [window, bucket_sec, series, components, errors, events, metrics, metric_names]
MetricPoint:
type: object
properties:
ts:
type: string
format: date-time
avg:
type: number
nullable: true
p50:
type: number
nullable: true
p95:
type: number
nullable: true
max:
type: number
nullable: true
count:
type: integer
required: [ts, count]
MetricSeriesResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/MetricPoint"
required: [items]