@adonis-agora/telescope 0.8.4 → 0.9.0
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 +19 -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/metrics/traces.d.ts +2 -0
- package/dist/src/metrics/traces.d.ts.map +1 -1
- package/dist/src/metrics/traces.js +18 -0
- package/dist/src/metrics/traces.js.map +1 -1
- package/dist/src/profiling/index.d.ts +1 -1
- package/dist/src/profiling/index.js +1 -1
- package/dist/src/request_watcher.d.ts +30 -0
- package/dist/src/request_watcher.d.ts.map +1 -1
- package/dist/src/request_watcher.js +26 -0
- package/dist/src/request_watcher.js.map +1 -1
- package/dist/src/ui/api.d.ts +5 -0
- package/dist/src/ui/api.d.ts.map +1 -1
- package/dist/src/ui/api.js +13 -0
- package/dist/src/ui/api.js.map +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,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telescope-access-mcp
|
|
3
|
+
description: >-
|
|
4
|
+
Dashboard access control and the MCP endpoint in @adonis-agora/telescope —
|
|
5
|
+
config/telescope_ui.ts authorize(ctx) hook vs credentials { token, basic },
|
|
6
|
+
dashboardAuth { secret, ttl, login } login screen, defaultAuthorize (allow outside
|
|
7
|
+
production, deny in production), 401-vs-403 fail-closed guard semantics,
|
|
8
|
+
redirect-wins behaviour, replay/queueActions/cpuProfiling.armEnabled mutation
|
|
9
|
+
gates, and config/telescope_mcp.ts JSON-RPC tools behind the same guard. Use for
|
|
10
|
+
"lock down the dashboard", "dashboard open in production", "401 on /telescope",
|
|
11
|
+
"login wall", "expose telescope to Claude Code", "replay returns 403".
|
|
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/dashboard/auth.mdx"
|
|
20
|
+
- "DavideCarvalho/adonis-telescope:docs/packages/mcp.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-telescope:docs/packages/ui.mdx"
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Dashboard access control & MCP
|
|
25
|
+
|
|
26
|
+
Every route serving recorded data runs an `authorize` guard first — every
|
|
27
|
+
`<path>/api/*` endpoint, the SSE stream, metrics and the SPA routes. Out of the box
|
|
28
|
+
the policy is **allow outside production, deny in production** until a credential or
|
|
29
|
+
hook permits it. The MCP subpath reuses exactly the same guard.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
The simplest production gate is a built-in credential; either `token` or `basic`,
|
|
34
|
+
or both:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// config/telescope_ui.ts
|
|
38
|
+
import env from '#start/env'
|
|
39
|
+
import { defineConfig } from '@adonis-agora/telescope/ui'
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
enabled: true,
|
|
43
|
+
path: '/telescope',
|
|
44
|
+
credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
curl -H 'Authorization: Bearer s3cr3t' http://localhost:3333/telescope/api/entries
|
|
50
|
+
open 'http://localhost:3333/telescope?token=s3cr3t' # ?token= is convenient but leaky
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Source: `docs/dashboard/auth.mdx` (Built-in credentials).
|
|
54
|
+
|
|
55
|
+
## Core patterns
|
|
56
|
+
|
|
57
|
+
### Pattern 1 — delegate to your app's auth
|
|
58
|
+
|
|
59
|
+
When you set `authorize`, the built-in `credentials` gate is ignored — your hook IS
|
|
60
|
+
the whole policy. It receives your **real** `HttpContext` through a framework-light
|
|
61
|
+
declared type, so narrow it to reach `auth`/`session` at runtime.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// config/telescope_ui.ts
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
async authorize(ctx) {
|
|
67
|
+
const { auth } = ctx as unknown as {
|
|
68
|
+
auth: { check(): Promise<boolean>; user?: { role?: string } }
|
|
69
|
+
}
|
|
70
|
+
await auth.check()
|
|
71
|
+
return auth.user?.role === 'admin'
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Guard outcomes: denied with no credential → `401` (+ `WWW-Authenticate`); denied
|
|
77
|
+
with a rejected credential → `403`; a throwing hook → `403` (fail-closed).
|
|
78
|
+
|
|
79
|
+
Source: `docs/dashboard/auth.mdx` (Delegate to your app's auth, Guard behaviour).
|
|
80
|
+
|
|
81
|
+
### Pattern 2 — a real login wall with dashboardAuth
|
|
82
|
+
|
|
83
|
+
For shared human access: a server-rendered login page plus a signed session cookie.
|
|
84
|
+
It composes WITH `authorize` (both must pass). Missing `secret` or `login` throws at
|
|
85
|
+
boot rather than shipping an un-mintable gate.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// config/telescope_ui.ts
|
|
89
|
+
import env from '#start/env'
|
|
90
|
+
import User from '#models/user'
|
|
91
|
+
import hash from '@adonisjs/core/services/hash'
|
|
92
|
+
import { defineConfig } from '@adonis-agora/telescope/ui'
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
dashboardAuth: {
|
|
96
|
+
secret: env.get('TELESCOPE_DASHBOARD_SECRET'), // HMAC-SHA256 key, 32+ bytes — required
|
|
97
|
+
ttl: '8h',
|
|
98
|
+
async login(username, password) {
|
|
99
|
+
const user = await User.findBy('email', username)
|
|
100
|
+
if (!user || !(await hash.verify(user.password, password))) return null
|
|
101
|
+
if (!user.isAdmin) return null
|
|
102
|
+
return { id: String(user.id), name: user.fullName } // session user, or null to deny
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This mounts unguarded-by-necessity `GET/POST <path>/login` and `GET <path>/logout`
|
|
109
|
+
routes (they mint/clear the session only — no recorded data sits behind them).
|
|
110
|
+
An unauthenticated page navigation gets a `302` to the login; an unauthenticated API
|
|
111
|
+
call gets a plain `401`.
|
|
112
|
+
|
|
113
|
+
Source: `docs/dashboard/auth.mdx` (Built-in login screen).
|
|
114
|
+
|
|
115
|
+
### Pattern 3 — redirect instead of answering JSON
|
|
116
|
+
|
|
117
|
+
If the hook sets a `location` header and then returns `false`, the guard leaves the
|
|
118
|
+
redirect alone instead of overwriting it with a `403` body.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
authorize: (ctx) => {
|
|
122
|
+
const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
|
|
123
|
+
if (auth.user?.isAdmin === true) return true
|
|
124
|
+
ctx.response.redirect('/login?next=/telescope')
|
|
125
|
+
return false // redirect wins only when the hook returns false
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Source: `docs/dashboard/auth.mdx` (Redirecting instead of answering JSON).
|
|
130
|
+
|
|
131
|
+
### Pattern 4 — expose the store to coding agents over MCP
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// config/telescope_mcp.ts
|
|
135
|
+
import env from '#start/env'
|
|
136
|
+
import { defineConfig } from '@adonis-agora/telescope/mcp'
|
|
137
|
+
|
|
138
|
+
export default defineConfig({
|
|
139
|
+
enabled: true,
|
|
140
|
+
path: '/telescope/mcp',
|
|
141
|
+
credentials: { token: env.get('TELESCOPE_MCP_TOKEN') }, // same guard as the dashboard
|
|
142
|
+
// tools: ['list_entries', 'get_trace', 'get_health'], // hand agents a narrower surface
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
claude mcp add --transport http telescope https://your-app.example.com/telescope/mcp \
|
|
148
|
+
--header 'Authorization: Bearer <TELESCOPE_MCP_TOKEN>'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Six read tools ship: `list_entries`, `get_entry`, `get_trace`, `get_waterfall`,
|
|
152
|
+
`get_health`, `diagnose_exception`. Redaction and sampling are never bypassed — an
|
|
153
|
+
agent sees exactly what the dashboard sees.
|
|
154
|
+
|
|
155
|
+
Source: `docs/packages/mcp.mdx`.
|
|
156
|
+
|
|
157
|
+
### Pattern 5 — gate the dangerous mutations
|
|
158
|
+
|
|
159
|
+
Replay, CPU-arm and queue retry/enqueue are each disabled by default and answer
|
|
160
|
+
`403` until their ui-config gate is flipped — even when the underlying feature is
|
|
161
|
+
enabled.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
export default defineConfig({
|
|
165
|
+
replay: { enabled: true }, // POST <path>/api/requests/:id/replay — re-runs a real request!
|
|
166
|
+
cpuProfiling: { armEnabled: true }, // POST <path>/api/profiles/arm
|
|
167
|
+
queueActions: { enabled: true }, // queue console retry/enqueue
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Source: `docs/packages/ui.mdx` (Configuration, Request replay),
|
|
172
|
+
`docs/packages/cpu-profiling.mdx` (arming callout).
|
|
173
|
+
|
|
174
|
+
## Common mistakes
|
|
175
|
+
|
|
176
|
+
### CRITICAL Relying on an obscure path instead of the guard
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Wrong — renaming the prefix is defense in depth, not authentication:
|
|
180
|
+
defineConfig({ path: '/__telescope' }) // production: denied anyway without a credential/hook
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// Correct — make a deliberate choice for production:
|
|
185
|
+
defineConfig({
|
|
186
|
+
path: '/__telescope',
|
|
187
|
+
credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Mechanism: the default policy denies production until a credential or `authorize`
|
|
192
|
+
hook permits it — but that deny is the *default*, never a guarantee you can skip
|
|
193
|
+
configuring; security notes call this out explicitly.
|
|
194
|
+
Source: `docs/dashboard/auth.mdx` (default policy warn + Security notes).
|
|
195
|
+
|
|
196
|
+
### MEDIUM Setting credentials AND an authorize hook expecting both to run
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// Wrong — the token gate silently stops mattering once authorize is set.
|
|
200
|
+
defineConfig({
|
|
201
|
+
credentials: { token: env.get('TELESCOPE_UI_TOKEN') },
|
|
202
|
+
authorize: () => true, // now the whole policy; credentials ignored
|
|
203
|
+
})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Correct — pick one: credentials for simple gates, authorize for real policies.
|
|
208
|
+
defineConfig({ credentials: { basic: { username: 'admin', password: env.get('TELESCOPE_UI_PASSWORD') } } })
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Mechanism: the docs state the built-in `credentials` gate is ignored whenever
|
|
212
|
+
`authorize` is set — your hook replaces it entirely (`dashboardAuth`, by contrast,
|
|
213
|
+
composes with `authorize`).
|
|
214
|
+
Source: `docs/dashboard/auth.mdx` (Delegate to your app's auth).
|
|
215
|
+
|
|
216
|
+
### MEDIUM Reaching for ctx.auth without narrowing the context
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// Wrong — does not type-check: the declared parameter is a minimal HTTP slice.
|
|
220
|
+
authorize: (ctx) => ctx.auth.user?.isAdmin === true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// Correct — narrow to the slice you rely on; the REAL HttpContext passes through.
|
|
225
|
+
authorize: (ctx) => {
|
|
226
|
+
const { auth } = ctx as unknown as { auth: { user?: { isAdmin?: boolean } } }
|
|
227
|
+
return auth.user?.isAdmin === true
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Mechanism: the guard's declared shape is framework-light (`request.qs()`,
|
|
232
|
+
`request.header()`, `response`) so any server can satisfy it; `auth`/`session`
|
|
233
|
+
exist at runtime only after narrowing.
|
|
234
|
+
Source: `docs/dashboard/auth.mdx` (narrowing info callout).
|
|
235
|
+
|
|
236
|
+
### HIGH Exposing MCP in production without a token credential
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Wrong — relies on the dev-open default; prod denials surface as opaque JSON-RPC errors.
|
|
240
|
+
defineConfig({ enabled: true, path: '/telescope/mcp' })
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// Correct — a token/basic credential for production.
|
|
245
|
+
defineConfig({
|
|
246
|
+
path: '/telescope/mcp',
|
|
247
|
+
credentials: { token: env.get('TELESCOPE_MCP_TOKEN') },
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Mechanism: MCP has no transport-level 401 — denial rides inside the JSON-RPC
|
|
252
|
+
envelope as `-32001` (`MCP_UNAUTHORIZED`), which compliant clients just report as a
|
|
253
|
+
failed call.
|
|
254
|
+
Source: `docs/packages/mcp.mdx` (guard info callout).
|
|
255
|
+
|
|
256
|
+
### MEDIUM Configuring dashboardAuth without secret or login
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Wrong — both keys are required; this throws at boot.
|
|
260
|
+
defineConfig({ dashboardAuth: { secret: env.get('TELESCOPE_DASHBOARD_SECRET') } })
|
|
261
|
+
defineConfig({ dashboardAuth: { login: verifyCreds } })
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// Correct — non-empty secret AND a login hook.
|
|
266
|
+
defineConfig({
|
|
267
|
+
dashboardAuth: {
|
|
268
|
+
secret: env.get('TELESCOPE_DASHBOARD_SECRET'),
|
|
269
|
+
ttl: '8h',
|
|
270
|
+
login: verifyCreds,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Mechanism: the cookie can never be minted without a signing key and a validator, so
|
|
276
|
+
the config fails closed at startup rather than shipping an un-loginable wall.
|
|
277
|
+
Source: `docs/dashboard/auth.mdx` (Built-in login screen table + warn callout).
|
|
278
|
+
|
|
279
|
+
See also: `telescope-ui-dashboard/SKILL.md` — the SPA behind these gates;
|
|
280
|
+
`telescope-alerts-ai/SKILL.md` — the diagnose route this guard covers.
|
|
@@ -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.
|