@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
197 lines • 8.72 kB
TypeScript
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