Agora
Packages

@adonis-agora/telescope/ui

The Telescope dashboard's read backend — a JSON API plus an SSE live-stream served from the headless TelescopeService and mounted onto your app's router behind a configurable auth guard. The dashboard page itself is the separate @adonis-agora/telescope-ui SPA, which consumes these routes.

This subpath is the dashboard's read backend. It mounts a JSON API and an SSE live-stream directly onto your AdonisJS router — no separate service — all served from the headless TelescopeService, behind a configurable auth guard. The dashboard page itself is the @adonis-agora/telescope-ui SPA, a pre-built React bundle that consumes these routes.

Install

The dashboard ships inside the one @adonis-agora/telescope package. Enable it when configuring telescope:

npm i @adonis-agora/telescope
node ace configure @adonis-agora/telescope   # then pick "UI" at the prompt

Selecting UI registers @adonis-agora/telescope/ui_provider and publishes config/telescope_ui.ts. That gives you the JSON API and the live stream — curl http://localhost:3333/telescope/api/entries works immediately.

It does not give you a page. http://localhost:3333/telescope answers 404 until you also install @adonis-agora/telescope-ui, which owns the mount root and serves the console that consumes these routes.

How it mounts

On boot the provider reads config/telescope_ui.ts and registers routes under the configured prefix on your app's router: the JSON API under <path>/api/*, the SSE stream at <path>/api/stream, and — when you configure dashboardAuth — the login and logout routes. Everything that serves recorded data runs the authorize guard first. It reads the same live store the watchers write to, so the API always reflects what was just captured.

It serves nothing at the prefix root itself: <path> and <path>/* belong to the @adonis-agora/telescope-ui SPA provider, which is why the two packages can be mounted, gated and upgraded independently.

If @adonis-agora/telescope isn't enabled/booted, the store slot is null and the provider registers no routes (warn-logged) rather than crashing — the dashboard simply doesn't exist until the core is running.

What it serves

RouteServes
GET <path>/api/entriesList (filterable, capped). Compact EntrySummary rows.
GET <path>/api/entries/:idOne entry with full content, or 404.
GET <path>/api/trace/:traceIdEvery entry on a trace.
GET <path>/api/stats{ count, topFamilies, topTags }.
GET <path>/api/retentionHow long data survives here — see Retention.
GET <path>/api/metaWhat this install can do — see Capability discovery.
GET <path>/api/stream (if stream enabled)SSE live-tail of newly recorded entries.
GET <path>/api/metrics/statsWindowed latency/throughput/error aggregates.
GET <path>/api/metrics/timeseriesBucketed series for the charts.
GET <path>/api/metrics/tracesRecent traces, newest first.
GET <path>/api/metrics/waterfall/:traceIdOne trace as a waterfall.
GET <path>/api/metrics/n-plus-one/:traceIdRepeated-query loops detected on one trace.
GET <path>/api/metrics/pulse (if Pulse enabled)Pulse rollup snapshot.
POST <path>/api/requests/:id/replay (if replay enabled)Replay a captured request.
POST <path>/api/exceptions/:id/diagnose (if ai configured)Diagnose one exception — see Diagnosing an exception.
GET <path>/api/ext/:ext/data/:provider (if any extension)One data provider's result.
GET/POST <path>/api/profiles* (if cpu_profiling installed)Captured CPU profiles + arm trigger.
GET <path>/api/schedules/liveRegistered schedules + computed next-run, joined with last-run — see Live Schedules.
GET/POST <path>/api/queues/live* (if queue-manager watcher enabled)Live queue list/inspect/retry/enqueue — see Live Queue Manager.

See the dashboard tour for the full API shape and query parameters.

Capability discovery

GET <path>/api/meta answers unconditionally and describes what this particular install can do, so a front end can hide the buttons that would fail:

{
  "data": {
    "entryTypes": [],
    "dashboards": [],
    "ai": { "enabled": true },
    "profiling": { "enabled": false },
    "queueManager": { "enabled": false }
  }
}

entryTypes and dashboards list what extensions have contributed — both empty arrays when none are installed. The three flags say whether AI diagnosis has a model to call, whether CPU profiling is available, and whether a live queue driver is wired. The shipped console uses them to decide whether to render the Diagnose button, the Profiles view and the queue console at all.

Retention

GET <path>/api/retention reports the retention posture — how aggressively data is deleted and which entry types are being sampled — so a viewer can tell "there is nothing here" apart from "it was already pruned":

{
  "data": {
    "enabled": true,
    "afterMs": 86400000,
    "keepLast": 10000,
    "intervalMs": 60000,
    "sampling": [{ "type": "query", "rate": 0.1 }]
  }
}

enabled is whether a pruner is armed at all; afterMs is the age cutoff, keepLast the count cap (null when unset) and intervalMs the cycle cadence. sampling lists only the entry types recording below 100% — a type kept at rate 1 is omitted, so an empty array means nothing is being sampled away. The endpoint reports configuration, not live pruner state: it never tells you when the last cycle ran.

The types are exported for anyone building against it:

import type {
  RetentionInfo,           // the `data` payload above
  RetentionOptions,        // { prune?, sampling? } — what the API is constructed with
  RetentionPruneOptions,   // { enabled, afterMs, keepLast?, intervalMs }
  RetentionSamplingRate,   // { type, rate }
} from '@adonis-agora/telescope/ui'

Diagnosing an exception

When @adonis-agora/telescope/ai is installed and has a model to call, POST <path>/api/exceptions/:id/diagnose runs a diagnosis over one exception entry and returns it as rendered Markdown:

curl -X POST http://localhost:3333/telescope/api/exceptions/01JX.../diagnose
# { "data": { "markdown": "**Probable cause** …", "cached": true } }

Diagnoses are cached per exception family, so asking twice about the same error costs one model call — cached: true tells you which answer you got. Append ?force=true to spend a fresh call anyway, which is what the console's re-run control does after you have changed the code.

The endpoint answers 404 when AI diagnosis isn't configured for this dashboard or the id isn't an exception (server or browser-reported), and 502 when the model call failed or ran past its timeout. This is the route behind the Diagnose button in the console's exception detail view.

Configuration

config/telescope_ui.ts
import { defineConfig } from '@adonis-agora/telescope/ui'

export default defineConfig({
  enabled: true,
  path: '/telescope',
  // authorize: (ctx) => {
  //   const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
  //   return auth.user?.isAdmin === true
  // },
  // credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
})
KeyDefaultDescription
enabledtrueMaster switch; false registers no routes.
path/telescopeURL prefix (normalized to leading slash, no trailing slash).
authorizedefault policyAccess-decision hook — Dashboard auth.
credentials{}Built-in token / HTTP Basic gate for the default policy.
dashboardAuthunsetBuilt-in login page + signed session cookie — Dashboard auth.
replay{ enabled: false }Request-replay gate — see Request replay.
cpuProfiling{ armEnabled: false }Gate for POST <path>/api/profiles/arm — see CPU profiling.
queueActions{ enabled: false }Gate for the queue console's retry/enqueue mutations — see Live Queue Manager.

Request replay

The dashboard can re-issue a captured request entry against your local server so you can reproduce it with one click. This is powerful and therefore disabled by default — replaying re-runs a real request, and a captured POST / DELETE will run again and may mutate application state. Opt in explicitly:

config/telescope_ui.ts
import { defineConfig } from '@adonis-agora/telescope/ui'

export default defineConfig({
  replay: {
    enabled: true,        // default false — a disabled replay endpoint answers 403
    // port: 3333,        // override the local target port (reverse-proxy / non-standard setups)
    // timeoutMs: 30_000,
  },
})

With it enabled, POST <path>/api/requests/:id/replay re-issues the entry and returns the replayed { status, durationMs, body }. Under the hood the endpoint calls the exported replayRequest(content, options?), whose safety posture is deliberately narrow:

GuardBehaviour
Same-origin onlyThe replay always targets 127.0.0.1:<port> — never an arbitrary URL, so it can't become an SSRF primitive. Only the captured path is reused. Port resolves in order: explicit replay.port → the live dashboard request's port → PORT env → 3333 (the AdonisJS default).
Credential strippingcookie, authorization, host and content-length are stripped (REPLAY_STRIPPED_HEADERS) — a replay never silently reuses the original caller's session.
Self-identifyingCarries x-telescope-replay: 1, and the outbound fetch is marked internal so the http-client watcher never records (or recurses on) it.
BoundedA 30s timeout (REPLAY_TIMEOUT_MS) and a 4 KB response-body cap (REPLAY_BODY_CAP). Never throws — a failed call resolves to status: 0 with an error.
import {
  replayRequest,
  REPLAY_BODY_CAP,          // 4096
  REPLAY_TIMEOUT_MS,        // 30_000
  REPLAY_STRIPPED_HEADERS,  // Set(['cookie','authorization','host','content-length'])
  type ReplayOptions, type ReplayResult, type ReplayTransport,
} from '@adonis-agora/telescope/ui'

const result: ReplayResult = await replayRequest(entry.content, { port: 3333 })
// { status: 200, durationMs: 12, body: '…up to 4 KB…' }

The Adonis request watcher captures only method + path (no query string, no request headers, no body — and even that content is redacted before storage), so a replay reconstructs exactly that: a same-method call to the same local path. There is deliberately nothing sensitive to forward upstream. The ReplayTransport type lets you inject a fetch-shaped transport (tests pass a fake so they never hit the network).

Build your own UI

The data lives behind the documented JSON API, so the shipped console is optional. Disable it and point your own front end (an admin page, a Grafana panel, a CLI) at the same <path>/api/* endpoints, or read TelescopeService directly. The @adonis-agora/telescope/ui subpath also exports its framework-light building blocks — TelescopeApi, the enforceGuard guard and the toSummary / buildQuery helpers — if you want to assemble a custom mount.

Mounting on something that isn't AdonisJS

The API handlers never see an AdonisJS HttpContext. They take a UiHttpContext, a two-field shape you can satisfy from any server:

import type { UiHttpContext, UiRequest, UiResponse } from '@adonis-agora/telescope/ui'

interface UiRequest {
  method(): string
  qs(): Record<string, unknown>
  header(name: string): string | undefined
}

interface UiResponse {
  status(code: number): UiResponse
  header(name: string, value: string): UiResponse
  getHeader(name: string): unknown
  send(body: unknown): unknown
}

getHeader is required, not optional. It is what lets the guard notice that your authorize hook already redirected before it writes a 403 body — see Redirecting instead of answering JSON. An adapter that omits it will not type-check.

Everything the built-in login flow needs is exported too, if you want to mount that surface by hand rather than letting the provider register it:

ExportWhat it does
resolveDashboardAuth(options)Validates and resolves a dashboardAuth block; null when unconfigured, throws when configured but unusable.
performLogin(auth, body, basePath)Runs the whole login decision with no HTTP types — returns ok (with the cookie value and where to go next), bad-request, or unauthorized.
readSession(auth, cookieValue)Verifies a session cookie, or null.
decideDashboardAuth(auth, cookieValue, mode, requestUrl)The guard decision itself: allow, redirect, or unauthorized.
enforceDashboardAuth(ctx, auth, mode, basePath)The AdonisJS-flavoured wrapper that also writes the 302 or the 401.
sanitizeReturnTo(candidate, fallback)The open-redirect guard on the post-login destination — only same-origin, root-relative paths survive.
renderLoginPage(basePath)The server-rendered login HTML.
SESSION_COOKIE_NAME'telescope_dashboard_session'.

Peer dependencies

Only @adonisjs/core (^7.3.0), which the core already requires. This subpath ships no front-end — it is the JSON API/SSE backend. The React SPA lives in the separate @adonis-agora/telescope-ui package, published pre-built.

On this page