UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

197 lines 8.72 kB
import type { ModuleRef } from '@nestjs/core'; import type { ResolvedCoreConfig } from '../config/options.js'; import type { Entry, RecordInput } from '../entry/entry.js'; import type { Watcher } from '../nest/watcher.js'; /** * The published, versioned extension contract for `@dudousxd/nestjs-telescope`. * * Extensions are objects (usually returned by a factory so they can take options) * registered via `TelescopeModule.forRoot({ extensions: [...] })`. The host runs * their hooks at module init. Hooks are **multi** (every extension runs; results * accumulate). Single-slot hooks are intentionally not part of 0.x — the registry * is shaped to add them when a consumer needs one. * * @remarks Semver 0.x — the shape may change until 1.0. Out-of-repo extensions * should pin a compatible `@dudousxd/nestjs-telescope` peer range. */ export interface TelescopeExtension { /** Unique id — used in conflict/collision errors and for deterministic ordering. */ name: string; /** Contribute watchers. Merged into the existing `forRoot.watchers` list. */ watchers?(ctx: ExtensionContext): Watcher[]; /** Contribute navigable entry types — makes the hard-coded UI ENTRY_TYPES dynamic. */ entryTypes?(ctx: ExtensionContext): ExtensionEntryType[]; /** Contribute declarative dashboard pages (the panel IR). */ dashboards?(ctx: ExtensionContext): DashboardSpec[]; /** Named server-side queries that panels bind to via `{ provider, query }`. */ dataProviders?(ctx: ExtensionContext): DataProvider[]; /** * Observe EVERY recorded input (pre-sampling, complete counts) — drives metrics * export. Fired synchronously on the hot path; keep it cheap. Isolated by the * host: a throw is swallowed and never affects capture. */ observeRecord?(input: RecordInput): void; /** * Observe each just-persisted (post-sampling) batch — drives span/trace export. * Awaited off the host path inside the flush chain; a throw/rejection is * swallowed and never breaks the flush. */ observeFlush?(entries: Entry[]): void | Promise<void>; } /** Read-only context handed to every extension hook, resolved at module init. */ export interface ExtensionContext { /** Resolve host services (e.g. a durable engine/store, or TELESCOPE_STORAGE). */ readonly moduleRef: ModuleRef; readonly config: ResolvedCoreConfig; } /** A navigable entry type contributed by an extension (subset of the UI's EntryTypeDef). */ export interface ExtensionEntryType { /** Backend `type` filter value, e.g. 'durable'. */ id: string; /** Nav label, e.g. 'Workflows'. */ label: string; /** Tailwind `bg-*` dot color for the nav, e.g. 'bg-amber-400'. */ dot: string; } /** Threshold coloring for a numeric panel. `direction` says which way is worse. */ export interface PanelThresholds { warn: number; bad: number; direction: 'up-bad' | 'down-bad'; } /** A group of panels rendered together with its own column count. */ export interface DashboardSection { title?: string; cols?: 2 | 3 | 4; panels: Panel[]; } /** A declarative dashboard page. */ export interface DashboardSpec { /** Stable route id, e.g. 'durable.workflows'. Globally unique across extensions. */ id: string; /** Nav label, e.g. 'Workflows'. */ label: string; /** Optional nav grouping header. */ navGroup?: string; /** Flat layout (back-compat). Prefer `sections` for hierarchy. */ panels: Panel[]; /** Sectioned layout. When present, the UI renders these instead of `panels`. */ sections?: DashboardSection[]; } /** A bind from a panel to a named server-side provider + an opaque query object. */ export interface DataBinding { /** Provider name, e.g. 'durable.timeseries'. Resolved on the server. */ provider: string; /** Opaque query passed through to the provider's `resolve`. */ query?: Record<string, unknown>; } /** * A deep-link out of a table cell (to the durable dashboard, a telescope trace, etc.). * * @remarks Two hrefs conventions: * - **In-app hash route** — an `href` starting with `#/` (e.g. `'#/traces/{traceId}'`) * is a route inside the Telescope SPA itself. The UI renders it as a plain * anchor; browsers treat a same-document `#`-only href as a same-document * navigation (URL hash update + `hashchange`, no page reload), which the * dashboard's `HashRouter` picks up — the same mechanism the built-in Entries * table and Entry detail page already use for their own trace links. Leave * `external` unset for these. * - **Host-console link** — an absolute path with no `#` (e.g. * `'/durable/runs/{runId}'`) targets a page in the HOST application (the app * embedding/linking to Telescope), not a Telescope route. This is a real * top-level navigation; set `external: true` when it should open in a new tab. * * The one confirmed in-app hash route today is the trace waterfall view: * `#/traces/{traceId}` (`traceId` is the row key to substitute), which renders * `TracePage` — the single-trace waterfall. Bridges that want to deep-link a * table row to "show me this trace" should target that exact shape. */ export interface LinkSpec { /** A URL template with `{key}` placeholders filled from the row, e.g. '/durable/runs/{runId}'. */ href: string; /** When true, open in a new tab. */ external?: boolean; } export interface Column { key: string; label: string; link?: LinkSpec; } export type Panel = { kind: 'stat'; title: string; data: DataBinding; format?: 'number' | 'percent' | 'duration' | 'rate'; accent?: string; /** When true, the provider also returns `spark: number[]` and the card draws a sparkline. */ spark?: boolean; thresholds?: PanelThresholds; } | { kind: 'timeseries'; title: string; data: DataBinding; series: string[]; style?: 'area' | 'stacked'; } | { kind: 'topN'; title: string; data: DataBinding; limit?: number; } | { kind: 'table'; title: string; data: DataBinding; columns: Column[]; /** * Opt into paged-table mode: the UI renders prev/next controls (+ "page X * of Y") and re-resolves this panel's provider with `query.page` (1-based) * and `query.limit` merged in on top of the panel's own static `data.query`. * The provider MUST then return `{ rows, total, page, limit }` instead of * a bare `{ rows }` — see {@link DataProvider.resolve}. Omit (or `false`) * for the existing bare-rows table, unchanged. */ paged?: boolean; } | { kind: 'distribution'; title: string; data: DataBinding; markers?: Array<'p50' | 'p95' | 'p99'>; format?: 'duration' | 'number'; } | { kind: 'gauge'; title: string; data: DataBinding; min?: number; max?: number; format?: 'number' | 'percent' | 'duration' | 'rate'; thresholds?: PanelThresholds; } | { kind: 'breakdown'; title: string; data: DataBinding; style?: 'donut' | 'bar'; }; /** A named server-side query a panel binds to. */ export interface DataProvider { /** Stable name referenced by a panel's `DataBinding.provider`, e.g. 'durable.timeseries'. */ name: string; /** * Resolve a panel's data. `query` is the panel's `DataBinding.query`. Return value * shape is per panel kind: * - stat → `{ value: number; delta?: number; deltaLabel?: string; spark?: number[] }` * - timeseries → `{ rows: Array<{ label: string } & Record<string, number>> }` * - topN → `{ items: Array<{ label: string; value: number; id?: string }> }` * - table → `{ rows: Array<Record<string, unknown>> }`, or — when the * panel declares `paged: true` — `{ rows, total, page, limit }` * (`page`/`limit` normally echo the requested `query.page` / * `query.limit`; `total` is the full, unpaginated row count so * the UI can compute "page X of Y") * - distribution → `{ buckets: Array<{ label: string; count: number }>; p50?: number; p95?: number; p99?: number }` * - gauge → `{ value: number; min?: number; max?: number }` * - breakdown → `{ segments: Array<{ label: string; value: number; color?: string }> }` */ resolve(query: Record<string, unknown> | undefined, ctx: ExtensionContext): Promise<unknown>; } /** Identity helper for authoring extensions with full type inference. */ export declare function defineTelescopeExtension(ext: TelescopeExtension): TelescopeExtension; //# sourceMappingURL=types.d.ts.map