Skip to content

Routers

Two factories, two Express routers. Never an app, never a server — the host mounts them, guards them if it wants to, and owns the surrounding middleware.

Both take a Telemetry instance. Neither builds its own storage.

js
app.use('/telemetry/ingest', createIngest({ telemetry: t }));
app.use('/telemetry',        createDashboard({ telemetry: t, viewerAdapter, mountPath: '/telemetry' }));

createIngest(options)

ts
function createIngest(opts: CreateIngestOptions): express.Router;

The untrusted write path. Batch-only, insert-gated rollups, pk_ never 4xxes, sk_ gets honest errors. See Ingest & keys for the model and Public HTTP API for the wire.

Options

OptionTypeDefault
telemetryTelemetryRequired.
contextAdapterContextAdapternone{ resolveContext(req): IngestContext | null | Promise<…> }. Consulted only for tenantMode: 'session' keys. A session-mode key with no adapter configured logs a warning and answers 202 (pk_) or 500 no_context_adapter (sk_).
maxRecordsnumber100Records per batch. Over it: pk_ keeps the head and counts the tail; sk_ gets 413.
bodyLimitstring'512kb'Passed to express.json(). Over it: pk_ gets 202; sk_ gets 413.
keyCacheMsnumber60000Key document cache TTL. Unknown ids are negative-cached too. Also the lag between revoking a key and writers noticing.

What it mounts

MethodPath
OPTIONS/CORS preflight. Reflects the origin, 204. Cannot be key-scoped — the browser has not sent Authorization yet.
POST/The batch endpoint.

Plus an error middleware that catches body-parser failures (bad JSON, oversize) and routes them to the right error surface — which is why auth resolves before express.json(), and why you must not mount your own body parser in front of this router.

The per-key rate limiter and the key cache are per router instance, in memory. Two Node processes grant two buckets. It is a storm cap, not an SLA.


createDashboard(options)

ts
function createDashboard(opts: CreateDashboardOptions): express.Router;

The read surface: /api/* plus the built React SPA with hashed assets and an SPA fallback. See The dashboard and Admin HTTP API.

Options

OptionTypeDefault
telemetryTelemetryRequired.
viewerAdapterViewerAdapterRequired — construction throws without it. { resolveViewer(req): Viewer | null | Promise<…> }.
subjectAdapterSubjectAdapternone{ describe(refs): Promise<Record<string, { label, href? }>> }. Absent, /api/subjects/describe returns { refs: {} } and the UI renders raw refs.
viewsViewSpec[][]Configured views — versioned in host code. Shadowed by saved views of the same name; shadow derived ones.
queryLimitsPartial<QueryLimits>DEFAULT_LIMITSPer-primitive caps.
onSlowQuery(info: { op, ms, params }) => voidnoneCalled when a read exceeds slowMs.
slowMsnumber500The threshold onSlowQuery fires above. Forwarded to createQueries.
cacheTtlMsnumber600000TTL of the in-process query cache. Ten minutes suits a dashboard someone is reading; a polling page wants it shorter. Forwarded.
cacheSizenumber60Cached results kept before the oldest is evicted — the bound on what the cache costs. Forwarded.
mountPathstring'/telemetry'Where the browser sees this router. Injected as <base href="…/">. Behind a prefix-stripping proxy this is the external path.
apiBasestringmountPathWhere the SPA calls /api. Injected as window.__TELEMETRY__.apiBase = "<apiBase>/api".
titlestring'Telemetry'Page title and sidebar brand.
spaDirstringdefaultSpaDir()Override the built SPA directory.

The query cache is per router instance, in memory, on the same terms as the ingest router's key cache: two Node processes hold two caches. It covers the four aggregating primitives — series, distribution, rollups, distinctCount — so cacheTtlMs is also the lag between a write landing and a chart moving. records, trace, journey, and funnel are never cached.

Throws at construction

telemetry: createDashboard requires a viewerAdapter — an unauthenticated
telemetry dashboard is a data leak with charts

A permissive default here would be a footgun that fires in production and looks fine in development. Failing at boot means it fires on your machine instead.

What it mounts

/api/*The JSON API — see Admin HTTP API. express.json({ limit: '64kb' }), then the viewer gate, then the routes, then a JSON error handler.
/_assets/*express.static(spaDir/_assets) with maxAge: 1y, immutable, index: false.
GET /.*The SPA shell. Reads spaDir/index.html, replaces the <!--telemetry-config--> marker with <base href="<mountPath>/"> and a window.__TELEMETRY__ config blob, and sends it with Cache-Control: no-store.

The injected JSON has < escaped to < — a </script> inside it would close the tag early.

The shell answers 503 (text/plain) when spaDir/index.html does not exist, with a line telling you to run npm run build. dist/ is not committed, so that is the normal state of a fresh clone.

The shell must never cache while _assets cache hard: a heuristically-cached index.html keeps referencing dead hashed assets across deploys, and the assets have content hashes in their names, so caching those forever is free.


defaultSpaDir()

ts
function defaultSpaDir(): string;

Resolves the bundled SPA directory. Tries ./ui (published builds run from dist/index.js, so the bundle sits alongside at dist/ui) then ../../dist/ui (running from source), returning the first whose index.html exists. Falls back to the last candidate so the 503 path can report a real, wrong-looking path rather than an empty string.

Two candidates rather than a build-time flag because a flag gets one of the two cases wrong, and the wrong one is the one you only hit after publishing.

Released under the MIT License.