@shaferllc/keel 0.68.0 → 0.77.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/AGENTS.md +2 -0
- package/README.md +15 -5
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +93 -0
- package/dist/api/resource.js +185 -0
- package/dist/core/application.js +6 -0
- package/dist/core/cache.d.ts +1 -2
- package/dist/core/cache.js +9 -2
- package/dist/core/cli/index.d.ts +28 -0
- package/dist/core/cli/index.js +427 -0
- package/dist/core/cli/stubs.d.ts +14 -0
- package/dist/core/cli/stubs.js +105 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/health.d.ts +2 -2
- package/dist/core/health.js +2 -2
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +25 -4
- package/dist/core/index.js +13 -3
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +28 -1
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +117 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.js +26 -5
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.js +15 -3
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +171 -1
- package/dist/core/testing.js +505 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +22 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +737 -3
- package/docs/api-resources.md +119 -0
- package/docs/configuration.md +74 -0
- package/docs/console.md +217 -13
- package/docs/database.md +101 -0
- package/docs/examples/api-resources.ts +86 -0
- package/docs/examples/configuration.ts +40 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/database.ts +90 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/logger.ts +74 -0
- package/docs/examples/mail.ts +103 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/testing.ts +150 -0
- package/docs/health.md +4 -4
- package/docs/i18n.md +302 -0
- package/docs/logger.md +156 -10
- package/docs/mail.md +73 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/storage.md +5 -3
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +3 -4
- package/docs/testing.md +252 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +2389 -241
- package/llms.txt +13 -1
- package/package.json +32 -6
package/docs/testing.md
CHANGED
|
@@ -90,6 +90,258 @@ kernel.use(requestLogger());
|
|
|
90
90
|
const client = testClient(kernel);
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
## Authenticated requests
|
|
94
|
+
|
|
95
|
+
The client's `withX` methods return a **copy**, so a client configured once can be
|
|
96
|
+
reused without leaking into other tests:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const authed = client.withToken("tok_123"); // Authorization: Bearer tok_123
|
|
100
|
+
|
|
101
|
+
await authed.get("/me");
|
|
102
|
+
await client.get("/me"); // still anonymous
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Method | Sends |
|
|
106
|
+
|--------|-------|
|
|
107
|
+
| `withToken(token)` | `Authorization: Bearer <token>` |
|
|
108
|
+
| `withBasicAuth(user, pass)` | `Authorization: Basic <base64>` |
|
|
109
|
+
| `withHeader(name, value)` / `withHeaders({…})` | any header |
|
|
110
|
+
| `withCookie(name, value)` / `withCookies({…})` | a `Cookie` header |
|
|
111
|
+
| `acceptJson()` | `Accept: application/json` |
|
|
112
|
+
|
|
113
|
+
## Forms and uploads
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await client.form("/login", { email: "a@b.com", password: "s3cret" }); // url-encoded
|
|
117
|
+
await client.multipart("/avatar", { file: new Blob([png]), name: "ada" }); // file upload
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## More response assertions
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
res.assertOk(); // 2xx
|
|
124
|
+
res.assertCreated(); // 201
|
|
125
|
+
res.assertNoContent(); // 204
|
|
126
|
+
res.assertUnauthorized(); // 401
|
|
127
|
+
res.assertForbidden(); // 403
|
|
128
|
+
res.assertNotFound(); // 404
|
|
129
|
+
res.assertUnprocessable(); // 422
|
|
130
|
+
res.assertServerError(); // 5xx
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`assertJsonContains` is a subset match** — the one you usually want. It pins the
|
|
134
|
+
fields the test is about and ignores the rest, so adding a field to a response
|
|
135
|
+
doesn't break twenty tests:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
res.assertJsonContains({ user: { email: "a@b.com" } });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`assertJson` still deep-equals the whole body, when that's what you mean.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
res.assertSee("Welcome back"); // body contains
|
|
145
|
+
res.assertDontSee("Sign up");
|
|
146
|
+
|
|
147
|
+
res.assertHeader("content-type", "application/json");
|
|
148
|
+
res.assertHeaderMissing("x-debug");
|
|
149
|
+
|
|
150
|
+
res.assertCookie("session"); // was set
|
|
151
|
+
res.assertCookie("session", "abc123"); // ...with this value
|
|
152
|
+
res.assertCookieMissing("admin");
|
|
153
|
+
|
|
154
|
+
res.dump(); // print status, headers, body — when you're stuck
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Validation
|
|
158
|
+
|
|
159
|
+
A failed `validate()` returns a 422 with per-field errors, so a test can assert on
|
|
160
|
+
the field rather than the message:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const res = await client.post("/users", { email: "nope" });
|
|
164
|
+
|
|
165
|
+
res.assertValidationErrors("email", "password");
|
|
166
|
+
res.assertNoValidationError("name");
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Test doubles
|
|
170
|
+
|
|
171
|
+
Keel's fakes swap out a real backend for a recording one, so a test can assert
|
|
172
|
+
that something *would* have happened without it actually happening — no email
|
|
173
|
+
sent, no card charged, no file uploaded.
|
|
174
|
+
|
|
175
|
+
| Fake | Replaces | Assertions |
|
|
176
|
+
|------|----------|------------|
|
|
177
|
+
| [`fakeMail()`](./mail.md#in-tests) | the mailer | `assertSent`, `assertQueued`, … |
|
|
178
|
+
| [`fakeQueue()`](./queues.md#in-tests) | the queue | `assertPushed`, `assertNothingPushed`, … |
|
|
179
|
+
| [`fakeDisk()`](./storage.md#testing) | a storage disk | `assertExists`, `assertContents`, … |
|
|
180
|
+
| [`events().fake()`](./events.md#testing) | the emitter | `assertEmitted`, `assertNotEmitted`, … |
|
|
181
|
+
| [`hash.fake()`](./hashing.md) | PBKDF2 | — (just makes it fast) |
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const mailer = fakeMail();
|
|
185
|
+
const queue = fakeQueue();
|
|
186
|
+
|
|
187
|
+
await registerUser({ email: "ada@example.com" });
|
|
188
|
+
|
|
189
|
+
mailer.assertQueued((m) => m.subject === "Welcome");
|
|
190
|
+
queue.assertPushed(SendWelcome);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
For anything else, `swap()` replaces a container binding:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
swap(PaymentGateway, () => new FakeGateway());
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Spies
|
|
200
|
+
|
|
201
|
+
The smallest double: a function that records how it was called.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { spy, spyOn, restoreSpies } from "@shaferllc/keel/core";
|
|
205
|
+
|
|
206
|
+
const send = spy<[string], void>();
|
|
207
|
+
notify(send);
|
|
208
|
+
|
|
209
|
+
assert.equal(send.callCount, 1);
|
|
210
|
+
assert.ok(send.calledWith("hello"));
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`spyOn` replaces a method on an object. It **calls through** by default — so you're
|
|
214
|
+
observing, not stubbing — until you tell it otherwise:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const charge = spyOn(gateway, "charge"); // still really charges
|
|
218
|
+
charge.returns(receipt); // now it doesn't
|
|
219
|
+
|
|
220
|
+
restoreSpies(); // put every spied method back
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Controlling time
|
|
224
|
+
|
|
225
|
+
Testing "this token expires in an hour" shouldn't take an hour.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { freezeTime, timeTravel, restoreTime } from "@shaferllc/keel/core";
|
|
229
|
+
|
|
230
|
+
freezeTime("2026-07-11T12:00:00Z");
|
|
231
|
+
|
|
232
|
+
const token = await jwt.sign({ sub: "1" }, { expiresIn: "1h" });
|
|
233
|
+
assert.ok(await jwt.verify(token)); // valid now
|
|
234
|
+
|
|
235
|
+
timeTravel(61 * 60 * 1000); // an hour and a minute later
|
|
236
|
+
assert.equal(await jwt.verify(token), null); // expired
|
|
237
|
+
|
|
238
|
+
restoreTime();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`freezeTime()` mocks `Date` and `Date.now()`. It does **not** mock timers — a
|
|
242
|
+
`setTimeout` still fires on the real clock — and `new Date("2020-01-01")` still
|
|
243
|
+
parses normally. Only "what time is it *now*" is frozen.
|
|
244
|
+
|
|
245
|
+
## Resetting state between tests
|
|
246
|
+
|
|
247
|
+
Keel's fakes, disks, queues, and cache are process-global, so one test can leak
|
|
248
|
+
into the next. `resetState()` puts it all back:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { resetState } from "@shaferllc/keel/core";
|
|
252
|
+
|
|
253
|
+
afterEach(() => resetState());
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
It restores every fake (mail, queue, disk, hash), unfreezes the clock, drops event
|
|
257
|
+
listeners, empties the cache, and gives you a fresh lock store. It does **not**
|
|
258
|
+
touch the database.
|
|
259
|
+
|
|
260
|
+
For that, `truncate()`:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
afterEach(() => truncate("comments", "posts", "users")); // children before parents
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It deletes rows rather than rolling back a transaction, so it works on every driver
|
|
267
|
+
(D1, Postgres, libSQL) instead of only the ones with savepoints.
|
|
268
|
+
|
|
269
|
+
## Database assertions
|
|
270
|
+
|
|
271
|
+
Assert against the database directly, rather than through an endpoint:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount } from "@shaferllc/keel/core";
|
|
275
|
+
|
|
276
|
+
await client.post("/users", { email: "ada@example.com" });
|
|
277
|
+
|
|
278
|
+
await assertDatabaseHas("users", { email: "ada@example.com" });
|
|
279
|
+
await assertDatabaseHas("users", { active: 1 }, 1); // exactly one match
|
|
280
|
+
await assertDatabaseMissing("users", { email: "deleted@example.com" });
|
|
281
|
+
await assertDatabaseCount("users", 1);
|
|
282
|
+
await assertDatabaseEmpty("sessions");
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
A failure tells you what it looked for and how many rows the table actually holds.
|
|
286
|
+
|
|
287
|
+
## Console tests
|
|
288
|
+
|
|
289
|
+
Run a command in-process — no subprocess, so it's fast and you can assert on it:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { runCommand } from "@shaferllc/keel/core";
|
|
293
|
+
import { run } from "@shaferllc/keel/cli";
|
|
294
|
+
import { createApplication } from "../bootstrap/app.js";
|
|
295
|
+
|
|
296
|
+
const result = await runCommand(() => run(["node", "keel", "routes"], { createApplication }));
|
|
297
|
+
|
|
298
|
+
result
|
|
299
|
+
.assertSucceeded() // exit code 0
|
|
300
|
+
.assertOutputContains("GET /users")
|
|
301
|
+
.assertOutputMatches(/POST\s+\/users/);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
You pass the command **in**, because a command needs an *application*, and only your
|
|
305
|
+
app knows how to build one. That's also why `run()` takes a `createApplication`
|
|
306
|
+
factory rather than importing one. Anything that prints and sets an exit code works,
|
|
307
|
+
so this is equally good for testing a function you wrote yourself.
|
|
308
|
+
|
|
309
|
+
`console.log`/`warn` are captured as stdout and `console.error` as stderr; a
|
|
310
|
+
command that *throws* is recorded as a failure rather than blowing up the test.
|
|
311
|
+
|
|
312
|
+
`assertFailed()`, `assertExitCode(n)`, and `assertErrorContains(text)` cover the
|
|
313
|
+
rest. `result.stdout`, `result.stderr`, and `result.exitCode` are there if you'd
|
|
314
|
+
rather assert by hand.
|
|
315
|
+
|
|
316
|
+
## Browser tests
|
|
317
|
+
|
|
318
|
+
Keel doesn't ship a browser driver — that's [Playwright](https://playwright.dev)'s
|
|
319
|
+
job, and wrapping it would only put a thinner API in front of a better one.
|
|
320
|
+
|
|
321
|
+
The test client injects requests *without a server*, which is what makes it fast;
|
|
322
|
+
a browser needs a real one. Start the app on a port, point Playwright at it, and
|
|
323
|
+
tear it down:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { serve } from "@hono/node-server";
|
|
327
|
+
import { chromium } from "playwright";
|
|
328
|
+
|
|
329
|
+
const server = serve({ fetch: new HttpKernel(app).build().fetch, port: 3001 });
|
|
330
|
+
const browser = await chromium.launch();
|
|
331
|
+
|
|
332
|
+
const page = await browser.newPage();
|
|
333
|
+
await page.goto("http://localhost:3001/login");
|
|
334
|
+
await page.fill("[name=email]", "ada@example.com");
|
|
335
|
+
await page.click("button[type=submit]");
|
|
336
|
+
await page.waitForURL("**/dashboard");
|
|
337
|
+
|
|
338
|
+
await browser.close();
|
|
339
|
+
server.close();
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Everything else on this page — the fakes, `freezeTime`, `resetState`, the database
|
|
343
|
+
assertions — works the same in a browser test, because it's the same process.
|
|
344
|
+
|
|
93
345
|
## API reference
|
|
94
346
|
|
|
95
347
|
### `testClient(target)`
|
package/docs/watch.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Watch
|
|
2
|
+
|
|
3
|
+
Keel Watch is a debug dashboard — a Telescope for Keel. It records the requests,
|
|
4
|
+
queries, exceptions, logs, jobs, mail, notifications, cache lookups, events, and
|
|
5
|
+
scheduled tasks flowing through your app, and shows them in a single-page UI at
|
|
6
|
+
`/watch`, with every request linked to the queries and logs it produced.
|
|
7
|
+
|
|
8
|
+
It ships as a Keel [package](./packages.md) and is its reference implementation:
|
|
9
|
+
one `register()` turns it on, and its watchers observe the framework's
|
|
10
|
+
instrumentation event stream — they patch nothing, so installing Watch changes no
|
|
11
|
+
behaviour, only visibility.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// bootstrap/providers.ts
|
|
17
|
+
import { WatchServiceProvider } from "@shaferllc/keel/watch";
|
|
18
|
+
|
|
19
|
+
export const providers = [AppServiceProvider, WatchServiceProvider];
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Publish the config and create the table:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
keel vendor:publish --tag watch-config # writes config/watch.ts
|
|
26
|
+
keel migrate # creates watch_entries
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then open `http://localhost:3000/watch`.
|
|
30
|
+
|
|
31
|
+
> Watch exposes requests, payloads, and stack traces. It is gated shut in
|
|
32
|
+
> production by default (see [Access](#access)) — keep it that way, or lock it
|
|
33
|
+
> down with your own check.
|
|
34
|
+
|
|
35
|
+
## What it records
|
|
36
|
+
|
|
37
|
+
A tab per type, each behind an on/off switch:
|
|
38
|
+
|
|
39
|
+
| Watcher | Records |
|
|
40
|
+
|---------|---------|
|
|
41
|
+
| Requests | method, path, status, duration, headers (auth/cookies redacted) |
|
|
42
|
+
| Queries | SQL, bindings, duration, connection; slow queries are tagged |
|
|
43
|
+
| Exceptions | class, message, stack, the request that threw |
|
|
44
|
+
| Logs | every log line, at any level |
|
|
45
|
+
| Mail | sent messages (subject, recipients, body) |
|
|
46
|
+
| Jobs | queued jobs as they complete or fail |
|
|
47
|
+
| Notifications | deliveries and their channels |
|
|
48
|
+
| Cache | hits and misses *(off by default — noisy)* |
|
|
49
|
+
| Events | app events *(off by default — noisy)* |
|
|
50
|
+
| Schedule | scheduled tasks as they run |
|
|
51
|
+
|
|
52
|
+
Clicking any entry shows its full detail and **everything else in its batch** —
|
|
53
|
+
the request it belonged to and every query, log, and exception that request
|
|
54
|
+
produced.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
`config/watch.ts` (publish it, then edit):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
export default {
|
|
62
|
+
enabled: env("WATCH_ENABLED", true),
|
|
63
|
+
path: "watch", // the dashboard mounts at /watch
|
|
64
|
+
storage: "database", // "database" persists; "memory" is a per-process ring
|
|
65
|
+
connection: undefined, // which DB connection (default when omitted)
|
|
66
|
+
table: "watch_entries",
|
|
67
|
+
limit: 100, // API page size; the memory ring is 10× this
|
|
68
|
+
sampling: 1, // record this fraction of entries (0–1)
|
|
69
|
+
slowQueryMs: 100, // queries at/above this are tagged "slow"
|
|
70
|
+
ignorePaths: [], // request paths to skip
|
|
71
|
+
retentionHours: 24, // keel watch:prune deletes entries older than this
|
|
72
|
+
watchers: { cache: false, event: false /* … the rest default on */ },
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Storage
|
|
77
|
+
|
|
78
|
+
- **`database`** (default) — a `watch_entries` table via any registered
|
|
79
|
+
connection. Survives restarts, shared across processes; needs `keel migrate`.
|
|
80
|
+
- **`memory`** — a per-process ring buffer. Zero setup, great for a single dev
|
|
81
|
+
process or the edge; entries vanish on restart and aren't shared.
|
|
82
|
+
|
|
83
|
+
## Access
|
|
84
|
+
|
|
85
|
+
By default the dashboard is open only when `app.debug` is on or the app isn't in
|
|
86
|
+
production. Anywhere else, lock it to your own check:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { Watch } from "@shaferllc/keel/watch";
|
|
90
|
+
import { auth } from "@shaferllc/keel/core";
|
|
91
|
+
|
|
92
|
+
Watch.auth(() => auth().check() && Boolean(auth().user()?.isAdmin));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The gate guards the JSON API too, so entries can't be read by anyone the
|
|
96
|
+
dashboard wouldn't show them to.
|
|
97
|
+
|
|
98
|
+
## Pruning
|
|
99
|
+
|
|
100
|
+
With database storage, keep the table small:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
keel watch:prune # delete entries older than retentionHours
|
|
104
|
+
keel watch:prune --hours=6
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Schedule it (see [Scheduling](./scheduling.md)) to run it automatically.
|
|
108
|
+
|
|
109
|
+
## How it works
|
|
110
|
+
|
|
111
|
+
Watch never wraps or monkey-patches the framework. The core emits a typed
|
|
112
|
+
[instrumentation event stream](./packages.md#observing-the-framework) —
|
|
113
|
+
`db.query`, `request.handled`, `exception`, `job.*`, and so on — and each watcher
|
|
114
|
+
is just a listener that turns an event into a stored entry, stamped with the
|
|
115
|
+
current request id so related entries group into one batch. Recording is
|
|
116
|
+
fire-and-forget: it never blocks or breaks the request it's watching, and the
|
|
117
|
+
store's own queries are filtered out so it never records itself.
|
|
118
|
+
```
|