@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.
- package/CHANGELOG.md +13 -0
- package/dist/src/ai/index.d.ts +1 -1
- package/dist/src/ai/index.js +1 -1
- package/dist/src/alerts/index.d.ts +1 -1
- package/dist/src/alerts/index.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/index.d.ts +1 -1
- package/dist/src/mcp/index.js +1 -1
- package/dist/src/profiling/index.d.ts +1 -1
- package/dist/src/profiling/index.js +1 -1
- package/dist/src/ui/index.d.ts +1 -1
- package/dist/src/ui/index.js +1 -1
- package/dist/src/watchers/index.d.ts +1 -1
- package/dist/src/watchers/index.js +1 -1
- package/package.json +6 -3
- package/skills/telescope-access-mcp/SKILL.md +280 -0
- package/skills/telescope-alerts-ai/SKILL.md +290 -0
- package/skills/telescope-setup/SKILL.md +255 -0
- package/skills/telescope-storage-retention/SKILL.md +292 -0
- package/skills/telescope-watchers/SKILL.md +317 -0
|
@@ -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.
|