mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 07:41:23 +00:00
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:
1 parent
6507eee606
commit
8cfddf9184
60 files changed
+3694
-30
No files matched your search
@@ -23,6 +23,8 @@ export type { BucketObjectListResponse } from './models/BucketObjectListResponse
|
||||
export type { Collection } from './models/Collection';
|
||||
export type { CollectionID } from './models/CollectionID';
|
||||
export type { CollectionListResponse } from './models/CollectionListResponse';
|
||||
export type { ComponentHealth } from './models/ComponentHealth';
|
||||
export type { ComponentID } from './models/ComponentID';
|
||||
export type { ConnectionID } from './models/ConnectionID';
|
||||
export type { CopyBucketObjectRequest } from './models/CopyBucketObjectRequest';
|
||||
export type { CreateApiKeyRequest } from './models/CreateApiKeyRequest';
|
||||
@@ -55,10 +57,16 @@ export type { Document } from './models/Document';
|
||||
export type { DocumentID } from './models/DocumentID';
|
||||
export type { DocumentListResponse } from './models/DocumentListResponse';
|
||||
export type { ErrorEnvelope } from './models/ErrorEnvelope';
|
||||
export type { EventSeriesBucket } from './models/EventSeriesBucket';
|
||||
export type { HealthStatus } from './models/HealthStatus';
|
||||
export { IngestEvent } from './models/IngestEvent';
|
||||
export type { IngestRequest } from './models/IngestRequest';
|
||||
export type { IngestResponse } from './models/IngestResponse';
|
||||
export type { Invitation } from './models/Invitation';
|
||||
export type { InvitationID } from './models/InvitationID';
|
||||
export type { MeResponse } from './models/MeResponse';
|
||||
export type { MetricPoint } from './models/MetricPoint';
|
||||
export type { MetricSeriesResponse } from './models/MetricSeriesResponse';
|
||||
export type { ObjectKey } from './models/ObjectKey';
|
||||
export type { OrganizationID } from './models/OrganizationID';
|
||||
export { OrganizationInvitation } from './models/OrganizationInvitation';
|
||||
@@ -76,6 +84,13 @@ export type { ProjectSummary } from './models/ProjectSummary';
|
||||
export type { ReadinessStatus } from './models/ReadinessStatus';
|
||||
export type { SchemaQuery } from './models/SchemaQuery';
|
||||
export type { TableQuery } from './models/TableQuery';
|
||||
export type { TelemetryComponent } from './models/TelemetryComponent';
|
||||
export type { TelemetryComponentListResponse } from './models/TelemetryComponentListResponse';
|
||||
export type { TelemetryEvent } from './models/TelemetryEvent';
|
||||
export type { TelemetryEventListResponse } from './models/TelemetryEventListResponse';
|
||||
export type { TelemetryIssue } from './models/TelemetryIssue';
|
||||
export type { TelemetryIssueListResponse } from './models/TelemetryIssueListResponse';
|
||||
export type { TelemetryStats } from './models/TelemetryStats';
|
||||
export type { UpdateBucketObjectRequest } from './models/UpdateBucketObjectRequest';
|
||||
export { UpdateBucketRequest } from './models/UpdateBucketRequest';
|
||||
export type { UpdateCollectionRequest } from './models/UpdateCollectionRequest';
|
||||
@@ -103,6 +118,8 @@ export { $BucketObjectListResponse } from './schemas/$BucketObjectListResponse';
|
||||
export { $Collection } from './schemas/$Collection';
|
||||
export { $CollectionID } from './schemas/$CollectionID';
|
||||
export { $CollectionListResponse } from './schemas/$CollectionListResponse';
|
||||
export { $ComponentHealth } from './schemas/$ComponentHealth';
|
||||
export { $ComponentID } from './schemas/$ComponentID';
|
||||
export { $ConnectionID } from './schemas/$ConnectionID';
|
||||
export { $CopyBucketObjectRequest } from './schemas/$CopyBucketObjectRequest';
|
||||
export { $CreateApiKeyRequest } from './schemas/$CreateApiKeyRequest';
|
||||
@@ -135,10 +152,16 @@ export { $Document } from './schemas/$Document';
|
||||
export { $DocumentID } from './schemas/$DocumentID';
|
||||
export { $DocumentListResponse } from './schemas/$DocumentListResponse';
|
||||
export { $ErrorEnvelope } from './schemas/$ErrorEnvelope';
|
||||
export { $EventSeriesBucket } from './schemas/$EventSeriesBucket';
|
||||
export { $HealthStatus } from './schemas/$HealthStatus';
|
||||
export { $IngestEvent } from './schemas/$IngestEvent';
|
||||
export { $IngestRequest } from './schemas/$IngestRequest';
|
||||
export { $IngestResponse } from './schemas/$IngestResponse';
|
||||
export { $Invitation } from './schemas/$Invitation';
|
||||
export { $InvitationID } from './schemas/$InvitationID';
|
||||
export { $MeResponse } from './schemas/$MeResponse';
|
||||
export { $MetricPoint } from './schemas/$MetricPoint';
|
||||
export { $MetricSeriesResponse } from './schemas/$MetricSeriesResponse';
|
||||
export { $ObjectKey } from './schemas/$ObjectKey';
|
||||
export { $OrganizationID } from './schemas/$OrganizationID';
|
||||
export { $OrganizationInvitation } from './schemas/$OrganizationInvitation';
|
||||
@@ -156,6 +179,13 @@ export { $ProjectSummary } from './schemas/$ProjectSummary';
|
||||
export { $ReadinessStatus } from './schemas/$ReadinessStatus';
|
||||
export { $SchemaQuery } from './schemas/$SchemaQuery';
|
||||
export { $TableQuery } from './schemas/$TableQuery';
|
||||
export { $TelemetryComponent } from './schemas/$TelemetryComponent';
|
||||
export { $TelemetryComponentListResponse } from './schemas/$TelemetryComponentListResponse';
|
||||
export { $TelemetryEvent } from './schemas/$TelemetryEvent';
|
||||
export { $TelemetryEventListResponse } from './schemas/$TelemetryEventListResponse';
|
||||
export { $TelemetryIssue } from './schemas/$TelemetryIssue';
|
||||
export { $TelemetryIssueListResponse } from './schemas/$TelemetryIssueListResponse';
|
||||
export { $TelemetryStats } from './schemas/$TelemetryStats';
|
||||
export { $UpdateBucketObjectRequest } from './schemas/$UpdateBucketObjectRequest';
|
||||
export { $UpdateBucketRequest } from './schemas/$UpdateBucketRequest';
|
||||
export { $UpdateCollectionRequest } from './schemas/$UpdateCollectionRequest';
|
||||
@@ -174,3 +204,4 @@ export { OrganizationsService } from './services/OrganizationsService';
|
||||
export { PlatformService } from './services/PlatformService';
|
||||
export { ProjectsService } from './services/ProjectsService';
|
||||
export { StorageService } from './services/StorageService';
|
||||
export { TelemetryService } from './services/TelemetryService';
|
||||
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ComponentHealth = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
last_seen_at?: string | null;
|
||||
last_status?: string;
|
||||
errors_24h: number;
|
||||
events_24h: number;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ComponentID = string;
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EventSeriesBucket = {
|
||||
ts: string;
|
||||
counts: Record<string, number>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type IngestEvent = {
|
||||
type: IngestEvent.type;
|
||||
/**
|
||||
* Auto-registered under the project on first use
|
||||
*/
|
||||
component?: string;
|
||||
kind?: IngestEvent.kind;
|
||||
severity?: string;
|
||||
message?: string;
|
||||
payload?: Record<string, any>;
|
||||
/**
|
||||
* Error grouping key; derived from message when omitted
|
||||
*/
|
||||
fingerprint?: string;
|
||||
ts?: string;
|
||||
};
|
||||
export namespace IngestEvent {
|
||||
export enum type {
|
||||
ERROR = 'error',
|
||||
METRIC = 'metric',
|
||||
LOG = 'log',
|
||||
HEARTBEAT = 'heartbeat',
|
||||
EVENT = 'event',
|
||||
}
|
||||
export enum kind {
|
||||
FRONTEND = 'frontend',
|
||||
BACKEND = 'backend',
|
||||
DATABASE = 'database',
|
||||
ANDROID = 'android',
|
||||
DESKTOP = 'desktop',
|
||||
WEB = 'web',
|
||||
OTHER = 'other',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { IngestEvent } from './IngestEvent';
|
||||
export type IngestRequest = (IngestEvent | {
|
||||
events: Array<IngestEvent>;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type IngestResponse = {
|
||||
accepted: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type MetricPoint = {
|
||||
ts: string;
|
||||
avg?: number | null;
|
||||
p50?: number | null;
|
||||
p95?: number | null;
|
||||
max?: number | null;
|
||||
count: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { MetricPoint } from './MetricPoint';
|
||||
export type MetricSeriesResponse = {
|
||||
items: Array<MetricPoint>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TelemetryComponent = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
last_seen_at?: string | null;
|
||||
last_status?: string;
|
||||
errors_24h: number;
|
||||
events_24h: number;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TelemetryComponent } from './TelemetryComponent';
|
||||
export type TelemetryComponentListResponse = {
|
||||
items: Array<TelemetryComponent>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TelemetryEvent = {
|
||||
id: number;
|
||||
project_id: string;
|
||||
component_id?: string | null;
|
||||
component_name?: string;
|
||||
type: string;
|
||||
severity: string;
|
||||
message: string;
|
||||
payload?: Record<string, any>;
|
||||
fingerprint?: string;
|
||||
ts: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TelemetryEvent } from './TelemetryEvent';
|
||||
export type TelemetryEventListResponse = {
|
||||
items: Array<TelemetryEvent>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TelemetryIssue = {
|
||||
fingerprint: string;
|
||||
message: string;
|
||||
component_id?: string | null;
|
||||
component_name?: string;
|
||||
count: number;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
severity: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { TelemetryIssue } from './TelemetryIssue';
|
||||
export type TelemetryIssueListResponse = {
|
||||
items: Array<TelemetryIssue>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ComponentHealth } from './ComponentHealth';
|
||||
import type { EventSeriesBucket } from './EventSeriesBucket';
|
||||
export type TelemetryStats = {
|
||||
window: number;
|
||||
bucket_sec: number;
|
||||
series: Array<EventSeriesBucket>;
|
||||
components: Array<ComponentHealth>;
|
||||
errors: number;
|
||||
events: number;
|
||||
metrics: number;
|
||||
metric_names: Array<string>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@primora/client",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
export type PrimoraEventType = "error" | "metric" | "log" | "heartbeat" | "event";
|
||||
export type PrimoraComponentKind =
|
||||
| "frontend" | "backend" | "database" | "android" | "desktop" | "web" | "other";
|
||||
|
||||
export interface PrimoraClientOptions {
|
||||
/** Primora deployment origin, e.g. http://localhost — /api/v1/ingest is appended */
|
||||
endpoint: string;
|
||||
/** project API key (prm_…) sent as X-Primora-Key */
|
||||
key: string;
|
||||
/** component name reported with every event */
|
||||
component: string;
|
||||
/** component kind used when auto-registering */
|
||||
kind?: PrimoraComponentKind;
|
||||
/** extra fields merged into every event payload (release, env, user opt-in flags…) */
|
||||
base?: Record<string, unknown>;
|
||||
/** batch flush interval ms (default 5000) */
|
||||
flushInterval?: number;
|
||||
}
|
||||
|
||||
interface QueuedEvent {
|
||||
type: PrimoraEventType;
|
||||
severity?: string;
|
||||
message?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
ts: string;
|
||||
}
|
||||
|
||||
export interface PrimoraClient {
|
||||
captureError(err: unknown, extra?: Record<string, unknown>): void;
|
||||
captureMetric(m: { name: string; value: number; unit?: string } & Record<string, unknown>): void;
|
||||
captureLog(level: string, message: string, attrs?: Record<string, unknown>): void;
|
||||
captureEvent(message: string, payload?: Record<string, unknown>): void;
|
||||
heartbeat(status?: "up" | "degraded" | "down", meta?: Record<string, unknown>): void;
|
||||
/** attach window.onerror + unhandledrejection (browser only) */
|
||||
installAuto(): void;
|
||||
flush(): void;
|
||||
}
|
||||
|
||||
export function createPrimoraClient(opts: PrimoraClientOptions): PrimoraClient {
|
||||
const endpoint = opts.endpoint.replace(/\/$/, "") + "/api/v1/ingest";
|
||||
const queue: QueuedEvent[] = [];
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const push = (e: QueuedEvent) => {
|
||||
queue.push(e);
|
||||
if (!timer) {
|
||||
timer = setTimeout(flush, opts.flushInterval ?? 5000);
|
||||
}
|
||||
};
|
||||
|
||||
function flush() {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (!queue.length) return;
|
||||
const events = queue.splice(0, queue.length).map((e) => ({
|
||||
...e,
|
||||
component: opts.component,
|
||||
kind: opts.kind,
|
||||
payload: { ...opts.base, ...e.payload },
|
||||
}));
|
||||
const body = JSON.stringify({ events });
|
||||
// fetch + keepalive survives page unload; sendBeacon can't set X-Primora-Key so it isn't used.
|
||||
fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Primora-Key": opts.key },
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
const client: PrimoraClient = {
|
||||
captureError(err, extra) {
|
||||
const e = err instanceof Error ? err : new Error(String(err));
|
||||
push({
|
||||
type: "error",
|
||||
severity: "error",
|
||||
message: e.message,
|
||||
payload: { stack: e.stack, name: e.name, ...extra },
|
||||
ts: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
captureMetric(m) {
|
||||
const { name, value, unit, ...rest } = m;
|
||||
push({ type: "metric", payload: { name, value, unit, ...rest }, ts: new Date().toISOString() });
|
||||
},
|
||||
captureLog(level, message, attrs) {
|
||||
push({ type: "log", severity: level, message, payload: attrs, ts: new Date().toISOString() });
|
||||
},
|
||||
captureEvent(message, payload) {
|
||||
push({ type: "event", message, payload, ts: new Date().toISOString() });
|
||||
},
|
||||
heartbeat(status = "up", meta) {
|
||||
push({ type: "heartbeat", payload: { status, ...meta }, ts: new Date().toISOString() });
|
||||
flush();
|
||||
},
|
||||
installAuto() {
|
||||
if (typeof window === "undefined") return;
|
||||
window.addEventListener("error", (ev) => client.captureError(ev.error ?? ev.message));
|
||||
window.addEventListener("unhandledrejection", (ev) => client.captureError(ev.reason));
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "hidden") flush();
|
||||
});
|
||||
},
|
||||
flush,
|
||||
};
|
||||
return client;
|
||||
}
|
||||
Reference in new issue
Block a user