@adonis-agora/telescope 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ ---
2
+ name: telescope-alerts-ai
3
+ description: >-
4
+ Alerting, AI diagnosis and client-error ingestion in @adonis-agora/telescope —
5
+ config/telescope_alerts.ts with AlertRule types (new-exception, every-exception,
6
+ exception-rate, stateful metric-threshold), channel helpers (slackChannel,
7
+ webhookChannel, consoleChannel, customChannel), every/cooldown/instanceId/
8
+ geoLookup; config/telescope_ai.ts + TelescopeAiDiagnoser Claude diagnosis cached
9
+ by familyHash (keyless = disabled); clientErrors sendBeacon endpoint. Use for
10
+ "Slack alert on new errors", "AI diagnose exceptions", "page on every error",
11
+ "report browser errors", "why didn't my alert fire".
12
+ license: MIT
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/telescope"
16
+ library_version: "0.8.4"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-telescope:docs/packages/alerts.mdx"
20
+ - "DavideCarvalho/adonis-telescope:docs/packages/ai.mdx"
21
+ - "DavideCarvalho/adonis-telescope:docs/packages/client-errors.mdx"
22
+ - "DavideCarvalho/adonis-telescope:packages/core/src/alerts/index.ts"
23
+ ---
24
+
25
+ # Alerting, AI diagnosis & client errors
26
+
27
+ The alerter polls the store on an interval, evaluates rules over recorded
28
+ `exception` entries, and fans each fire out to every configured channel. The AI
29
+ subpath attaches a Claude root-cause diagnosis to an exception, cached by family.
30
+ Client-error ingestion lets browsers report their own failures into the same
31
+ pipeline. All three are subpaths of the one package — enable them via
32
+ `node ace configure @adonis-agora/telescope` (pick **Alerts** / **AI**).
33
+
34
+ ## Setup
35
+
36
+ ```ts
37
+ // config/telescope_alerts.ts
38
+ import env from '#start/env'
39
+ import { defineConfig } from '@adonis-agora/telescope/alerts'
40
+
41
+ export default defineConfig({
42
+ channels: [{ type: 'slack', url: env.get('TELESCOPE_SLACK_WEBHOOK') }],
43
+ rules: [{ type: 'new-exception', window: '1h' }],
44
+ dashboardUrl: 'https://telescope.example.com/', // enables Slack deep links
45
+ every: '30s', // poll cadence — alerts are as late as the interval
46
+ cooldown: '15m', // per-rule / per-family re-notify suppression
47
+ })
48
+ ```
49
+
50
+ ```ts
51
+ // config/telescope_ai.ts
52
+ import env from '#start/env'
53
+ import { defineConfig } from '@adonis-agora/telescope/ai'
54
+
55
+ export default defineConfig({
56
+ apiKey: env.get('ANTHROPIC_API_KEY'), // validate as Env.schema.string.optional()
57
+ model: 'claude-sonnet-4-6', // default; claude-opus-4-8 / claude-haiku-4-5-20251001
58
+ maxTokens: 1024,
59
+ })
60
+ ```
61
+
62
+ Durations are validated at boot — an unparseable `every`/`cooldown`/`window`
63
+ (`'15min'`, a typo) **throws**, fail-closed. Valid units: `ms`, `s`, `m`, `h`, `d`.
64
+
65
+ Source: `docs/packages/alerts.mdx` (Configuration),
66
+ `docs/packages/ai.mdx` (Configuration).
67
+
68
+ ## Core patterns
69
+
70
+ ### Pattern 1 — pick rules deliberately
71
+
72
+ ```ts
73
+ rules: [
74
+ { type: 'new-exception', window: '1h' }, // first sighting of a family (+ recurrences after it)
75
+ { type: 'every-exception', window: '15m' },// EVERY server + browser error, cooldown-limited per family;
76
+ // window is display-only (labels the occurrence count)
77
+ { type: 'exception-rate', window: '5m', threshold: 10 },
78
+ {
79
+ type: 'metric-threshold', // stateful raise/auto-resolve, evaluated on every poll
80
+ metric: 'request-p99-ms', // or query-p95-ms, cache-hit-rate, exception-count, ...
81
+ window: '5m',
82
+ comparator: 'gte',
83
+ threshold: 800,
84
+ minSamples: 10, // guard percentiles against a single slow request
85
+ },
86
+ ]
87
+ ```
88
+
89
+ `new-exception` dedup is a bounded per-process map — in a multi-replica deploy the
90
+ same family reports "new" once per replica, so budget `cooldown` accordingly.
91
+
92
+ Source: `docs/packages/alerts.mdx` (Rules, Deduplication).
93
+
94
+ ### Pattern 2 — diagnose an exception with its trace context
95
+
96
+ `TelescopeAiDiagnoser` is always resolvable from the container — even when disabled,
97
+ when `diagnose` resolves `null` — so you can inject it without a null check.
98
+
99
+ ```ts
100
+ // app/controllers/diagnose_controller.ts
101
+ import { inject } from '@adonisjs/core'
102
+ import type { HttpContext } from '@adonisjs/core/http'
103
+ import { TelescopeService } from '@adonis-agora/telescope'
104
+ import { TelescopeAiDiagnoser } from '@adonis-agora/telescope/ai'
105
+
106
+ @inject()
107
+ export default class DiagnoseController {
108
+ constructor(
109
+ private telescope: TelescopeService,
110
+ private diagnoser: TelescopeAiDiagnoser,
111
+ ) {}
112
+
113
+ async show({ params, response }: HttpContext) {
114
+ const entry = await this.telescope.find(params.id)
115
+ if (!entry || entry.type !== 'exception') {
116
+ return response.notFound({ error: 'Not an exception entry' })
117
+ }
118
+ const related = entry.traceId ? await this.telescope.byTrace(entry.traceId) : []
119
+ const diagnosis = await this.diagnoser.diagnose(entry, { related })
120
+ // → { cause, fix, confidence, model, cached } | null — never throws
121
+ return response.json(diagnosis)
122
+ }
123
+ }
124
+ ```
125
+
126
+ Diagnoses cache per `familyHash` (bounded LRU, 24h TTL); pass `{ force: true }` to
127
+ spend a fresh call after changing code.
128
+
129
+ Source: `docs/packages/ai.mdx` (Use it), `docs/recipes/ai-exception-diagnosis.mdx`.
130
+
131
+ ### Pattern 3 — a custom channel sink
132
+
133
+ Channels fan out concurrently; one failing never blocks the others.
134
+
135
+ ```ts
136
+ import { defineConfig, customChannel, slackChannel } from '@adonis-agora/telescope/alerts'
137
+ import env from '#start/env'
138
+
139
+ export default defineConfig({
140
+ channels: [
141
+ slackChannel(env.get('TELESCOPE_SLACK_WEBHOOK'), { username: 'Telescope' }),
142
+ customChannel(async (alert) => { await pagerduty.trigger(alert) }, 'pagerduty'),
143
+ ],
144
+ rules: [{ type: 'new-exception', window: '1h' }],
145
+ })
146
+ ```
147
+
148
+ Exception alerts carry `{ cause, fix, confidence, model }` when AI is configured —
149
+ a guarded hook, so a slow model never delays or breaks the page.
150
+
151
+ Source: `docs/packages/alerts.mdx` (Channels).
152
+
153
+ ### Pattern 4 — ingest browser errors
154
+
155
+ Disabled by default; the route only exists once you enable it. Report with
156
+ `navigator.sendBeacon`; entries become `client_exception` records through the same
157
+ redaction/sampling/prune pipeline as server exceptions.
158
+
159
+ ```ts
160
+ // config/telescope.ts
161
+ export default defineConfig({
162
+ clientErrors: {
163
+ enabled: true,
164
+ path: '/telescope/client-errors',
165
+ maxBodyBytes: 32_768, // larger bodies rejected 413 before validation
166
+ rateLimit: { perMinute: 60 }, // per-IP token bucket; over → 429
167
+ },
168
+ })
169
+ ```
170
+
171
+ ```ts
172
+ // Browser reporter
173
+ window.addEventListener('error', (event) => {
174
+ navigator.sendBeacon(
175
+ '/telescope/client-errors',
176
+ JSON.stringify({
177
+ message: event.message,
178
+ name: event.error?.name ?? null,
179
+ stack: event.error?.stack ?? null,
180
+ url: location.href,
181
+ userAgent: navigator.userAgent,
182
+ }),
183
+ )
184
+ })
185
+ ```
186
+
187
+ Only `message` is required; `clientIp` is filled server-side, never trusted from
188
+ the body.
189
+
190
+ Source: `docs/packages/client-errors.mdx`.
191
+
192
+ ## Common mistakes
193
+
194
+ ### HIGH Listing two rules of the same exception type
195
+
196
+ ```ts
197
+ // Wrong — the second new-exception rule is SILENTLY ignored.
198
+ rules: [
199
+ { type: 'new-exception', window: '1h' },
200
+ { type: 'new-exception', window: '24h' },
201
+ ]
202
+ ```
203
+
204
+ ```ts
205
+ // Correct — at most one of each exception rule; express variations via window/threshold.
206
+ rules: [
207
+ { type: 'new-exception', window: '1h' },
208
+ { type: 'exception-rate', window: '5m', threshold: 10 },
209
+ ]
210
+ ```
211
+
212
+ Mechanism: `new-exception`, `every-exception` and `exception-rate` are looked up
213
+ once by type; duplicates never evaluate (`metric-threshold` is exempt).
214
+ Source: `docs/packages/alerts.mdx` (When rules do and don't run).
215
+
216
+ ### HIGH Shipping AI config without a resolvable key
217
+
218
+ ```ts
219
+ // Wrong — keyless install looks configured but silently does nothing:
220
+ export default defineConfig({ model: 'claude-opus-4-8' }) // no apiKey, no client
221
+ const diagnosis = await diagnoser.diagnose(entry) // always null
222
+ ```
223
+
224
+ ```ts
225
+ // Correct — source the key from validated env; check isConfigured() when unsure.
226
+ export default defineConfig({ apiKey: env.get('ANTHROPIC_API_KEY') })
227
+ ```
228
+
229
+ Mechanism: a configured-but-keyless install is treated as disabled — `diagnose`
230
+ resolves `null` forever and the dashboard hides the Diagnose button, with no error
231
+ anywhere.
232
+ Source: `docs/packages/ai.mdx` (warn callout), `docs/reference/configuration.mdx` (AI).
233
+
234
+ ### MEDIUM Expecting sampled-out exceptions to page
235
+
236
+ ```ts
237
+ // Wrong — sampling drops most queries AND some exceptions before any rule sees them.
238
+ sampling: { default: 0.1 },
239
+ rules: [{ type: 'new-exception', window: '1h' }],
240
+ ```
241
+
242
+ ```ts
243
+ // Correct — tail-sampling keeps errors/slow regardless of rate.
244
+ sampling: { default: 0.1, exception: { rate: 0.1, keepErrors: true } },
245
+ ```
246
+
247
+ Mechanism: only what was recorded is alertable — sampling and the overload shed run
248
+ before the alerter's poll, and polling itself adds up to `every` of latency.
249
+ Source: `docs/packages/alerts.mdx` (How it hooks in).
250
+
251
+ ### MEDIUM Short cooldown with every-exception
252
+
253
+ ```ts
254
+ // Wrong — a tight loop of the same error re-pages once per cooldown... which is 1 minute.
255
+ rules: [{ type: 'every-exception' }],
256
+ cooldown: '1m',
257
+ ```
258
+
259
+ ```ts
260
+ // Correct — tune cooldown to your on-call tolerance; different families still page immediately.
261
+ rules: [{ type: 'every-exception', window: '15m' }],
262
+ cooldown: '10m',
263
+ ```
264
+
265
+ Mechanism: cooldown is the *only* thing rate-limiting repeats per family (on its own
266
+ independent clock from `new-exception`), so a short one on a noisy family is a
267
+ self-inflicted alert storm.
268
+ Source: `docs/packages/alerts.mdx` (cooldown info callout).
269
+
270
+ ### MEDIUM Probing for the client-error endpoint while it is disabled
271
+
272
+ ```sh
273
+ # Wrong — expecting a 404 that proves wiring:
274
+ curl -X POST http://localhost:3333/telescope/client-errors
275
+ # → generic route-not-found; can't tell whether telescope owns this prefix
276
+ ```
277
+
278
+ ```ts
279
+ // Correct — enable it, then verify the 204:
280
+ clientErrors: { enabled: true }
281
+ // curl -X POST ... → 204 No Content once registered
282
+ ```
283
+
284
+ Mechanism: unlike an always-mounted controller, the provider registers the POST
285
+ route only when `clientErrors.enabled` is true — while off the endpoint genuinely
286
+ does not exist.
287
+ Source: `docs/packages/client-errors.mdx` (Disabled by default warn callout).
288
+
289
+ See also: `telescope-watchers/SKILL.md` — capture gaps become alert gaps;
290
+ `telescope-access-mcp/SKILL.md` — the diagnose endpoint rides behind the dashboard guard.
@@ -0,0 +1,255 @@
1
+ ---
2
+ name: telescope-setup
3
+ description: >-
4
+ Set up @adonis-agora/telescope in an AdonisJS app. Covers node ace configure
5
+ codemods (telescope_provider + TelescopeMiddleware on the server stack +
6
+ config/telescope.ts), defineConfig keys (enabled/store/stores/watchers/redact),
7
+ the storage.memory and storage.lucid drivers, the headless TelescopeService query
8
+ API (list/find/byTrace/count/topFamilies/topTags/getHealth), recordException for
9
+ non-HTTP paths, opt-in requestCapture body gates, and why /telescope 404s without
10
+ @adonis-agora/telescope-ui. Use for "install telescope", "telemetry is empty",
11
+ "query captured entries", "capture request bodies", "AdonisJS observability".
12
+ license: MIT
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/telescope"
16
+ library_version: "0.8.4"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-telescope:README.md"
20
+ - "DavideCarvalho/adonis-telescope:docs/getting-started.mdx"
21
+ - "DavideCarvalho/adonis-telescope:docs/packages/core.mdx"
22
+ - "DavideCarvalho/adonis-telescope:docs/reference/configuration.mdx"
23
+ ---
24
+
25
+ # Setting up @adonis-agora/telescope
26
+
27
+ `@adonis-agora/telescope` is Laravel Telescope-style **headless** observability for
28
+ AdonisJS: it records every inbound HTTP request, every unhandled exception and every
29
+ `agora:<lib>:<event>` diagnostics publish as a queryable `Entry`. The core ships no
30
+ page — mounting a UI is a separate package (`telescope-ui-dashboard` skill).
31
+
32
+ ## Setup
33
+
34
+ ```sh
35
+ pnpm add @adonis-agora/telescope
36
+ node ace configure @adonis-agora/telescope
37
+ ```
38
+
39
+ `configure` runs codemods that register `@adonis-agora/telescope/telescope_provider`
40
+ in `adonisrc.ts`, plug `TelescopeMiddleware` onto the **server** middleware stack
41
+ (so it wraps the whole pipeline and observes the final status), and publish
42
+ `config/telescope.ts`. That alone records `request` + `diagnostic` entries.
43
+
44
+ The published config is all-defaults:
45
+
46
+ ```ts
47
+ // config/telescope.ts
48
+ import { defineConfig, storage } from '@adonis-agora/telescope'
49
+
50
+ export default defineConfig({
51
+ enabled: true, // master switch
52
+ store: 'memory', // which driver in `stores` is active
53
+ stores: {
54
+ memory: storage.memory({ limit: 1000 }), // bounded ring buffer, lost on restart
55
+ // lucid: storage.lucid({ connection: 'pg' }), // persistent SQL store (optional peer)
56
+ },
57
+ watchers: ['request', 'diagnostics'], // omit one to disable it
58
+ })
59
+ ```
60
+
61
+ Read entries back from anywhere via the container:
62
+
63
+ ```ts
64
+ import { TelescopeService } from '@adonis-agora/telescope'
65
+
66
+ const telescope = await app.container.make(TelescopeService)
67
+
68
+ await telescope.list({ type: 'request', limit: 50 }) // newest-first
69
+ await telescope.byTrace('abc123') // one request's whole story
70
+ await telescope.list({ tag: 'lib:billing', search: 'invoice' })
71
+ await telescope.topFamilies(10, 'diagnostic') // busiest lib:event pairs
72
+ ```
73
+
74
+ ## Core patterns
75
+
76
+ ### Pattern 1 — the headless API inside a controller
77
+
78
+ Controllers have no `app` of their own; use `@inject()` so the container resolves
79
+ the constructor dependency.
80
+
81
+ ```ts
82
+ // app/controllers/inspector_controller.ts
83
+ import { inject } from '@adonisjs/core'
84
+ import type { HttpContext } from '@adonisjs/core/http'
85
+ import { TelescopeService } from '@adonis-agora/telescope'
86
+
87
+ @inject()
88
+ export default class InspectorController {
89
+ constructor(private telescope: TelescopeService) {}
90
+
91
+ async index({ response }: HttpContext) {
92
+ return response.json(await this.telescope.list({ limit: 50 }))
93
+ }
94
+ }
95
+ ```
96
+
97
+ `EntryQuery` composes filters with AND: `{ type, tag, familyHash, traceId, before,
98
+ after, search, limit }`. `search` matches JSON content **and** tags, so a request
99
+ matches by url, a diagnostic by event name, a query by SQL — all from one box.
100
+ For React usage in a custom console, see the `telescope-ui-dashboard` skill.
101
+
102
+ Source: `docs/getting-started.mdx` (Step 3).
103
+
104
+ ### Pattern 2 — manual exception capture outside HTTP
105
+
106
+ Queue workers, ace commands and `app/exceptions/handler.ts` are not wrapped by the
107
+ middleware. `recordException` reads the live store with nothing injected, is a no-op
108
+ when Telescope is off, and never throws.
109
+
110
+ ```ts
111
+ // app/exceptions/handler.ts
112
+ import { recordException } from '@adonis-agora/telescope'
113
+
114
+ async report(error: unknown, ctx: HttpContext) {
115
+ recordException(error, { method: ctx.request.method(), url: ctx.request.url() })
116
+ return super.report(error, ctx)
117
+ }
118
+ ```
119
+
120
+ Exceptions group by `exceptionFamilyHash` = `name:message:topStackFrame`, so alerts
121
+ and the dashboard's exception groups stay deterministic across processes.
122
+
123
+ Source: `docs/concepts/capture.mdx` (Manual capture), `docs/packages/core.mdx`.
124
+
125
+ ### Pattern 3 — opt-in request-body capture
126
+
127
+ By default a `request` entry has **no `body` field at all**. Turn it on with gates
128
+ that run before redaction; a rejected body becomes a marker string like
129
+ `[Skipped: 200000 bytes > 131072 bytes]`.
130
+
131
+ ```ts
132
+ // config/telescope.ts
133
+ export default defineConfig({
134
+ requestCapture: {
135
+ maxBodyBytes: 65_536,
136
+ skipBodyContentTypes: ['multipart/form-data'],
137
+ skipBody: ({ url }) => url.startsWith('/webhooks/'),
138
+ },
139
+ })
140
+ ```
141
+
142
+ Captured bodies still pass through central redaction — but a body never captured is
143
+ the only body that definitely cannot leak.
144
+
145
+ Source: `docs/packages/core.mdx` (Request body capture).
146
+
147
+ ### Pattern 4 — production storage
148
+
149
+ Switch to the built-in Lucid driver when entries must survive restarts (see the
150
+ `telescope-storage-retention` skill for the migration and pruning):
151
+
152
+ ```sh
153
+ pnpm add @adonisjs/lucid
154
+ node ace migration:run # create_telescope_entries_table, shipped by configure
155
+ ```
156
+
157
+ ```ts
158
+ export default defineConfig({
159
+ store: 'lucid',
160
+ stores: { lucid: storage.lucid() }, // or storage.lucid({ connection: 'pg' })
161
+ })
162
+ ```
163
+
164
+ Source: `docs/getting-started.mdx` (Step 4).
165
+
166
+ ## Common mistakes
167
+
168
+ ### HIGH Expecting a dashboard page from the core alone
169
+
170
+ ```ts
171
+ // Wrong — nothing mounts a page; http://localhost:3333/telescope answers 404.
172
+ // adonisrc.ts
173
+ providers: [
174
+ () => import('@adonis-agora/telescope/telescope_provider'),
175
+ // ...app providers
176
+ ]
177
+ ```
178
+
179
+ ```ts
180
+ // Correct — install @adonis-agora/telescope-ui and register its provider AFTER ui_provider.
181
+ providers: [
182
+ () => import('@adonis-agora/telescope/telescope_provider'),
183
+ () => import('@adonis-agora/telescope/ui_provider'), // JSON API + SSE
184
+ () => import('@adonis-agora/telescope-ui/telescope_ui_dashboard_provider'), // the SPA
185
+ ]
186
+ ```
187
+
188
+ Mechanism: the core `ui` subpath serves only `<path>/api/*`; the prefix root belongs
189
+ to the separate SPA package, whose provider must also be registered after it.
190
+ Source: `docs/packages/ui.mdx` (Install), `docs/packages/telescope-ui.mdx` (warn).
191
+
192
+ ### HIGH Assuming request bodies are captured by default
193
+
194
+ ```ts
195
+ // Wrong — agents assume they can read the payload that broke checkout:
196
+ const entry = await telescope.find(id)
197
+ entry.content.body // undefined — bodies are opt-in
198
+ ```
199
+
200
+ ```ts
201
+ // Correct — enable requestCapture explicitly (gates run before redaction).
202
+ defineConfig({
203
+ requestCapture: { maxBodyBytes: 65_536 },
204
+ })
205
+ ```
206
+
207
+ Mechanism: the right default keeps passwords/card numbers out of storage; the entry
208
+ records method/url/status/duration only until you opt in per environment.
209
+ Source: `docs/packages/core.mdx` (Request body capture).
210
+
211
+ ### HIGH Hand-rolling the provider without the server-stack middleware
212
+
213
+ ```ts
214
+ // Wrong — provider registered but TelescopeMiddleware missing: no requests recorded,
215
+ // no exceptions captured, and nothing warns about it.
216
+ providers: [
217
+ () => import('@adonis-agora/telescope/telescope_provider'),
218
+ ]
219
+ ```
220
+
221
+ ```sh
222
+ # Correct — let the configure codemod wire both pieces.
223
+ node ace configure @adonis-agora/telescope
224
+ # verifies: telescope_provider present AND TelescopeMiddleware on the server stack
225
+ ```
226
+
227
+ Mechanism: capture comes from `TelescopeMiddleware` on the `server` middleware
228
+ stack; the provider alone starts only the boot-time watchers, so the timeline stays
229
+ empty while everything looks installed.
230
+ Source: `docs/reference/configuration.mdx` (Providers table), `docs/getting-started.mdx` Step 1.
231
+
232
+ ### MEDIUM Resolving TelescopeService via this.app inside a controller
233
+
234
+ ```ts
235
+ // Wrong — a controller has no `app`; this does not compile.
236
+ constructor(private app: ApplicationService) {}
237
+ async index() {
238
+ const telescope = await this.app.container.make(TelescopeService)
239
+ }
240
+ ```
241
+
242
+ ```ts
243
+ // Correct — constructor injection through @inject().
244
+ @inject()
245
+ export default class InspectorController {
246
+ constructor(private telescope: TelescopeService) {}
247
+ }
248
+ ```
249
+
250
+ Mechanism: `@inject()` asks the container to build the controller's dependencies
251
+ from their constructor types; `TelescopeService` arrives already resolved.
252
+ Source: `docs/getting-started.mdx` (Step 3 note).
253
+
254
+ See also: `telescope-ui-dashboard/SKILL.md` — mounting the page this setup lacks;
255
+ `telescope-storage-retention/SKILL.md` — swapping the dev-only memory driver.