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,225 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { IngestRequest } from '../models/IngestRequest';
import type { IngestResponse } from '../models/IngestResponse';
import type { MetricSeriesResponse } from '../models/MetricSeriesResponse';
import type { TelemetryComponentListResponse } from '../models/TelemetryComponentListResponse';
import type { TelemetryEventListResponse } from '../models/TelemetryEventListResponse';
import type { TelemetryIssueListResponse } from '../models/TelemetryIssueListResponse';
import type { TelemetryStats } from '../models/TelemetryStats';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class TelemetryService {
/**
* Ingest one telemetry event or a batch of up to 500
* @returns IngestResponse Accepted event count
* @throws ApiError
*/
public static ingestTelemetry({
requestBody,
}: {
requestBody: IngestRequest,
}): CancelablePromise<IngestResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/ingest',
body: requestBody,
mediaType: 'application/json',
errors: {
400: `Error response`,
401: `Error response`,
413: `Error response`,
},
});
}
/**
* @returns TelemetryEventListResponse Telemetry events, newest first
* @throws ApiError
*/
public static listTelemetryEvents({
projectId,
type,
component,
fingerprint,
before,
limit,
}: {
projectId: string,
type?: 'error' | 'metric' | 'log' | 'heartbeat' | 'event',
component?: string,
fingerprint?: string,
before?: number,
limit?: number,
}): CancelablePromise<TelemetryEventListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/events',
path: {
'projectID': projectId,
},
query: {
'type': type,
'component': component,
'fingerprint': fingerprint,
'before': before,
'limit': limit,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns TelemetryIssueListResponse Error groups by fingerprint
* @throws ApiError
*/
public static listTelemetryIssues({
projectId,
days,
}: {
projectId: string,
days?: number,
}): CancelablePromise<TelemetryIssueListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/issues',
path: {
'projectID': projectId,
},
query: {
'days': days,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns TelemetryComponentListResponse Registered telemetry components
* @throws ApiError
*/
public static listTelemetryComponents({
projectId,
}: {
projectId: string,
}): CancelablePromise<TelemetryComponentListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/components',
path: {
'projectID': projectId,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns void
* @throws ApiError
*/
public static deleteTelemetryComponent({
projectId,
componentId,
}: {
projectId: string,
componentId: string,
}): CancelablePromise<void> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/projects/{projectID}/components/{componentID}',
path: {
'projectID': projectId,
'componentID': componentId,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns TelemetryStats Aggregated telemetry statistics
* @throws ApiError
*/
public static getTelemetryStats({
projectId,
window,
}: {
projectId: string,
window?: '1h' | '24h' | '7d' | '30d',
}): CancelablePromise<TelemetryStats> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/telemetry/stats',
path: {
'projectID': projectId,
},
query: {
'window': window,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns MetricSeriesResponse Aggregated metric series
* @throws ApiError
*/
public static getTelemetryMetricSeries({
projectId,
name,
window,
}: {
projectId: string,
name: string,
window?: '1h' | '24h' | '7d' | '30d',
}): CancelablePromise<MetricSeriesResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/telemetry/metrics/series',
path: {
'projectID': projectId,
},
query: {
'name': name,
'window': window,
},
errors: {
401: `Error response`,
},
});
}
/**
* Server-sent events stream of live telemetry. EventSource clients pass `?api_key=` or `?token=` since headers cannot be set.
* @returns any text/event-stream of TelemetryEvent messages
* @throws ApiError
*/
public static streamTelemetryEvents({
projectId,
apiKey,
token,
}: {
projectId: string,
apiKey?: string,
token?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/telemetry/stream',
path: {
'projectID': projectId,
},
query: {
'api_key': apiKey,
'token': token,
},
errors: {
401: `Error response`,
},
});
}
}