@spfn/monitor 0.1.0-beta.17 → 0.1.0-beta.19
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/LICENSE +1 -1
- package/README.md +348 -126
- package/dist/index.d.ts +12 -12
- package/dist/index.js.map +1 -1
- package/dist/nextjs/client.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js.map +1 -1
- package/package.json +10 -9
- package/dist/{index-BiN0PoSx.d.ts → index-C9IUDNIv.d.ts} +10 -10
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,28 +1,54 @@
|
|
|
1
|
-
# @spfn/monitor
|
|
1
|
+
# @spfn/monitor — DB-backed error tracking, logging, and admin dashboard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DB-persisted error tracking with fingerprint deduplication and state-based Slack
|
|
4
|
+
notifications, a pluggable developer-log store, superadmin admin routes, and ready-made
|
|
5
|
+
React dashboard components. Integrates into an SPFN server via `defineServerConfig()`
|
|
6
|
+
(`onError` middleware + lifecycle) and a package router mounted with `.packages([...])`.
|
|
4
7
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **DB-backed error tracking**: Fingerprint-based deduplication with automatic grouping
|
|
8
|
-
- **State-based notifications**: Slack alerts only on new errors and reopened errors (no duplicates)
|
|
9
|
-
- **Developer logging**: Pluggable log storage with DB default
|
|
10
|
-
- **Admin API**: Superadmin-only routes for managing errors and viewing logs
|
|
11
|
-
- **React dashboard**: Ready-to-use monitoring UI components
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
8
|
+
## Install
|
|
14
9
|
|
|
15
10
|
```bash
|
|
16
11
|
pnpm add @spfn/monitor
|
|
17
12
|
```
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
Peer dep: `next` (`^15 || ^16`, optional — only needed for the `nextjs/client` components).
|
|
15
|
+
Workspace deps `@spfn/core`, `@spfn/auth`, `@spfn/notification` come transitively.
|
|
16
|
+
|
|
17
|
+
## Import paths
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
There are **four** entry points (from `package.json` `exports`):
|
|
22
20
|
|
|
23
21
|
```typescript
|
|
22
|
+
// Client-safe: API client + shared types/consts
|
|
23
|
+
import { monitorApi, type MonitorRouter, type MonitorStats } from '@spfn/monitor';
|
|
24
|
+
|
|
25
|
+
// Server-only: router, integrations, services, repos, entities (uses node:crypto, DB)
|
|
26
|
+
import { monitorRouter, createMonitorErrorHandler, writeLog } from '@spfn/monitor/server';
|
|
27
|
+
|
|
28
|
+
// Config: code-level overrides + env getters + schema
|
|
29
|
+
import { configureMonitor, getMonitorConfig } from '@spfn/monitor/config';
|
|
30
|
+
|
|
31
|
+
// React components ('use client') — Next.js only
|
|
32
|
+
import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Importing `@spfn/monitor/server` runs `@spfn/monitor/config` as a side effect (env registry
|
|
36
|
+
is built). Never import `/server` or `/config`'s `env`-touching code into client/edge bundles.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Setup (SPFN server)
|
|
41
|
+
|
|
42
|
+
Three integration points, all on `defineServerConfig()` + the app router:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/server/server.config.ts
|
|
24
46
|
import { defineServerConfig } from '@spfn/core/server';
|
|
25
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
createMonitorErrorHandler,
|
|
49
|
+
createMonitorLifecycle,
|
|
50
|
+
} from '@spfn/monitor/server';
|
|
51
|
+
import { appRouter } from './router';
|
|
26
52
|
|
|
27
53
|
export default defineServerConfig()
|
|
28
54
|
.middleware({ onError: createMonitorErrorHandler() })
|
|
@@ -31,202 +57,398 @@ export default defineServerConfig()
|
|
|
31
57
|
.build();
|
|
32
58
|
```
|
|
33
59
|
|
|
34
|
-
|
|
60
|
+
Mount `monitorRouter` as a **package router** on your app router (so its `/_monitor/admin/*`
|
|
61
|
+
routes are served, but stay out of `AppRouter`'s client types — call them via `monitorApi`):
|
|
35
62
|
|
|
36
63
|
```typescript
|
|
64
|
+
// src/server/router.ts
|
|
65
|
+
import { defineRouter } from '@spfn/core/route';
|
|
37
66
|
import { monitorRouter } from '@spfn/monitor/server';
|
|
67
|
+
import { authRouter } from '@spfn/auth/server';
|
|
38
68
|
|
|
39
|
-
export const appRouter = defineRouter({
|
|
69
|
+
export const appRouter = defineRouter({ /* your routes */ })
|
|
40
70
|
.packages([authRouter, monitorRouter]);
|
|
41
71
|
```
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
> `monitorRouter` is built with `defineRouter({...})` — there is no `mergeRouters` export in
|
|
74
|
+
> `@spfn/core/route`. Use `.packages([monitorRouter])`, not `mergeRouters(...)`.
|
|
75
|
+
|
|
76
|
+
### Database migration
|
|
77
|
+
|
|
78
|
+
`monitorSchema = createSchema('@spfn/monitor')` → the PostgreSQL schema **`spfn_monitor`**
|
|
79
|
+
with tables `error_groups`, `error_events`, `logs`.
|
|
44
80
|
|
|
45
81
|
```bash
|
|
46
|
-
spfn db
|
|
82
|
+
pnpm spfn db push # dev: push schema directly
|
|
83
|
+
# or, with migration history (production):
|
|
84
|
+
pnpm spfn db generate && pnpm spfn db migrate
|
|
47
85
|
```
|
|
48
86
|
|
|
49
|
-
|
|
87
|
+
`@spfn/monitor` ships pre-generated SQL under `migrations/` (declared via `package.json`
|
|
88
|
+
`spfn.migrations.dir`); SPFN's CLI picks up the package's entities (`spfn.schemas`).
|
|
89
|
+
|
|
90
|
+
---
|
|
50
91
|
|
|
51
92
|
## Configuration
|
|
52
93
|
|
|
53
|
-
### Environment
|
|
94
|
+
### Environment variables
|
|
54
95
|
|
|
55
|
-
|
|
56
|
-
# Slack webhook for error notifications (optional)
|
|
57
|
-
SPFN_MONITOR_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
|
96
|
+
All optional. Resolution order is **code config > env > built-in default**.
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
| Variable | Default | Description |
|
|
99
|
+
|----------|---------|-------------|
|
|
100
|
+
| `SPFN_MONITOR_SLACK_WEBHOOK_URL` | — | Slack webhook for error notifications (falls back to `@spfn/notification` default) |
|
|
101
|
+
| `SPFN_MONITOR_ERROR_RETENTION_DAYS` | `90` | Days to retain error events |
|
|
102
|
+
| `SPFN_MONITOR_LOG_RETENTION_DAYS` | `30` | Days to retain logs |
|
|
103
|
+
| `SPFN_MONITOR_MIN_STATUS_CODE` | `500` | Minimum HTTP status code tracked as an error |
|
|
61
104
|
|
|
62
|
-
|
|
63
|
-
SPFN_MONITOR_LOG_RETENTION_DAYS=30
|
|
105
|
+
The webhook URL is a secret — keep it in `.env.server` (server-only), never a committed file.
|
|
64
106
|
|
|
65
|
-
|
|
66
|
-
SPFN_MONITOR_MIN_STATUS_CODE=500
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Code Configuration
|
|
107
|
+
### Code config
|
|
70
108
|
|
|
71
109
|
```typescript
|
|
72
110
|
import { configureMonitor } from '@spfn/monitor/config';
|
|
73
111
|
|
|
74
112
|
configureMonitor({
|
|
75
|
-
slackWebhookUrl:
|
|
113
|
+
slackWebhookUrl: process.env.SPFN_MONITOR_SLACK_WEBHOOK_URL, // override
|
|
76
114
|
errorRetentionDays: 90,
|
|
77
115
|
logRetentionDays: 30,
|
|
78
116
|
minStatusCode: 500,
|
|
79
117
|
});
|
|
80
118
|
```
|
|
81
119
|
|
|
82
|
-
|
|
120
|
+
`MonitorConfig` keys: `slackWebhookUrl?`, `errorRetentionDays?`, `logRetentionDays?`,
|
|
121
|
+
`minStatusCode?`. Getters resolve config > env > default:
|
|
122
|
+
`getMonitorConfig()`, `getSlackWebhookUrl()`, `getErrorRetentionDays()`,
|
|
123
|
+
`getLogRetentionDays()`, `getMinStatusCode()`. The validated env proxy is exported as `env`,
|
|
124
|
+
the schema as `monitorEnvSchema`.
|
|
125
|
+
|
|
126
|
+
> Retention values are *exposed* config, but this package does **not** ship a purge
|
|
127
|
+
> scheduler. `LogStore.purge(olderThan)` and `logsRepository.deleteOlderThan(date)` exist;
|
|
128
|
+
> wire up your own cron/job if you want automatic retention enforcement.
|
|
83
129
|
|
|
84
|
-
|
|
130
|
+
---
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
## Error tracking
|
|
133
|
+
|
|
134
|
+
`createMonitorErrorHandler(options?)` returns an `onError` callback (signature
|
|
135
|
+
`(err: Error, ctx) => Promise<void>`) — a drop-in replacement for
|
|
136
|
+
`createErrorSlackNotifier` from `@spfn/notification`. On each error at/above `minStatusCode`
|
|
137
|
+
it calls `trackError`, which does state-based deduplication:
|
|
138
|
+
|
|
139
|
+
| Existing group state | Action | Slack? |
|
|
140
|
+
|----------------------|--------|--------|
|
|
141
|
+
| none (new fingerprint) | create group + event | yes (`new`) |
|
|
142
|
+
| `active` / `ignored` | increment count + create event | no |
|
|
143
|
+
| `resolved` | reopen → `active`, increment, create event | yes (`reopened`) |
|
|
144
|
+
|
|
145
|
+
Errors below `minStatusCode` are skipped. Slack/event failures are caught and logged — they
|
|
146
|
+
never break the response.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
createMonitorErrorHandler({
|
|
150
|
+
minStatusCode: 500, // default: getMinStatusCode() (env or 500)
|
|
151
|
+
environment: process.env.NODE_ENV, // shown in the Slack title, e.g. "[production]"
|
|
152
|
+
extractMetadata: (err, ctx) => ({ // stored on the error_event row
|
|
153
|
+
serverInstance: process.env.HOSTNAME,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`MonitorErrorHandlerOptions`: `minStatusCode?`, `environment?`, `extractMetadata?(err, ctx)`.
|
|
89
159
|
|
|
90
160
|
### Fingerprinting
|
|
91
161
|
|
|
92
|
-
|
|
162
|
+
`generateFingerprint(name, message, path)` → `SHA-256(name:message:path)` sliced to the
|
|
163
|
+
first **16 hex chars**. Same name+message+path ⇒ same group. The `path` component means the
|
|
164
|
+
same error from different routes is grouped separately.
|
|
93
165
|
|
|
94
|
-
### Manual
|
|
166
|
+
### Manual tracking
|
|
95
167
|
|
|
96
168
|
```typescript
|
|
97
169
|
import { trackError } from '@spfn/monitor/server';
|
|
98
170
|
|
|
99
|
-
await trackError(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
171
|
+
await trackError(
|
|
172
|
+
error, // Error
|
|
173
|
+
{ // ErrorTrackingContext
|
|
174
|
+
statusCode: 500,
|
|
175
|
+
path: '/api/example',
|
|
176
|
+
method: 'POST',
|
|
177
|
+
requestId: 'req_123', // optional
|
|
178
|
+
userId: 'user_42', // optional, string
|
|
179
|
+
headers: { /* ... */ }, // optional
|
|
180
|
+
query: { /* ... */ }, // optional
|
|
181
|
+
environment: 'production', // optional
|
|
182
|
+
},
|
|
183
|
+
{ custom: 'metadata' }, // optional 3rd arg → error_event.metadata
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ErrorTrackingContext` requires `statusCode`, `path`, `method`; the rest are optional.
|
|
188
|
+
|
|
189
|
+
### Status management
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { updateErrorGroupStatus } from '@spfn/monitor/server';
|
|
193
|
+
|
|
194
|
+
await updateErrorGroupStatus(groupId, 'resolved'); // sets resolved_at
|
|
195
|
+
await updateErrorGroupStatus(groupId, 'ignored');
|
|
196
|
+
await updateErrorGroupStatus(groupId, 'active'); // reopen
|
|
105
197
|
```
|
|
106
198
|
|
|
107
|
-
|
|
199
|
+
Throws if the group id does not exist. Statuses: `ERROR_GROUP_STATUSES` =
|
|
200
|
+
`['active', 'resolved', 'ignored']` (type `ErrorGroupStatus`).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Developer logging
|
|
108
205
|
|
|
109
206
|
```typescript
|
|
110
|
-
import { writeLog, queryLogs } from '@spfn/monitor/server';
|
|
207
|
+
import { writeLog, queryLogs, monitor } from '@spfn/monitor/server';
|
|
111
208
|
|
|
112
|
-
// Write a log entry
|
|
113
209
|
await writeLog({
|
|
114
|
-
level: 'info',
|
|
210
|
+
level: 'info', // LogLevel: debug|info|warn|error|fatal
|
|
115
211
|
message: 'User signed in',
|
|
116
|
-
source: 'auth',
|
|
117
|
-
|
|
118
|
-
|
|
212
|
+
source: 'auth', // optional
|
|
213
|
+
requestId: 'req_123', // optional
|
|
214
|
+
userId: 'user_42', // optional, string
|
|
215
|
+
metadata: { provider: 'google' }, // optional
|
|
119
216
|
});
|
|
120
217
|
|
|
121
|
-
//
|
|
122
|
-
|
|
218
|
+
await monitor.log({ level: 'warn', message: 'Rate limit approaching' }); // shorthand → writeLog
|
|
219
|
+
|
|
220
|
+
const logs = await queryLogs({ // LogFilters
|
|
123
221
|
level: 'error',
|
|
124
222
|
source: 'payment',
|
|
125
|
-
|
|
223
|
+
search: 'timeout', // ILIKE over message + source
|
|
224
|
+
requestId, userId,
|
|
225
|
+
dateFrom: new Date('2024-01-01'),
|
|
226
|
+
dateTo: new Date(),
|
|
227
|
+
limit: 50, // unbounded if omitted
|
|
228
|
+
offset: 0,
|
|
126
229
|
});
|
|
127
230
|
```
|
|
128
231
|
|
|
129
|
-
|
|
232
|
+
`writeLog` / `queryLogs` return `Log` / `Log[]`. `LOG_LEVELS` =
|
|
233
|
+
`['debug','info','warn','error','fatal']` (type `LogLevel`).
|
|
234
|
+
|
|
235
|
+
### Custom log store
|
|
130
236
|
|
|
131
|
-
|
|
237
|
+
Swap the default DB store for any backend implementing `LogStore`:
|
|
132
238
|
|
|
133
239
|
```typescript
|
|
134
|
-
import { setLogStore } from '@spfn/monitor/server';
|
|
240
|
+
import { setLogStore, getLogStore, type LogStore } from '@spfn/monitor/server';
|
|
135
241
|
|
|
136
|
-
|
|
137
|
-
async write(entry)
|
|
138
|
-
async query(filters)
|
|
139
|
-
async purge(olderThan) { /*
|
|
140
|
-
}
|
|
242
|
+
const s3Store: LogStore = {
|
|
243
|
+
async write(entry) { /* NewLog → Log */ },
|
|
244
|
+
async query(filters) { /* LogFilters → Log[] */ },
|
|
245
|
+
async purge(olderThan) { /* Date → number deleted */ },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
setLogStore(s3Store);
|
|
141
249
|
```
|
|
142
250
|
|
|
143
|
-
|
|
251
|
+
`setLogStore` is process-global and affects `writeLog`/`queryLogs` immediately. The default
|
|
252
|
+
store writes to the `logs` table. The admin `GET /_monitor/admin/logs` route also goes
|
|
253
|
+
through the current store.
|
|
144
254
|
|
|
145
|
-
|
|
255
|
+
---
|
|
146
256
|
|
|
147
|
-
|
|
148
|
-
|--------|------|-------------|
|
|
149
|
-
| GET | `/_monitor/admin/errors` | List error groups (filter by status, path, search) |
|
|
150
|
-
| GET | `/_monitor/admin/errors/:id` | Error group detail + recent events |
|
|
151
|
-
| PATCH | `/_monitor/admin/errors/:id` | Update error status (resolve/ignore/reopen) |
|
|
152
|
-
| GET | `/_monitor/admin/errors/:id/events` | List events for an error group |
|
|
153
|
-
| GET | `/_monitor/admin/logs` | Query logs (filter by level, source, search) |
|
|
154
|
-
| GET | `/_monitor/admin/stats` | Dashboard statistics |
|
|
257
|
+
## Admin API routes
|
|
155
258
|
|
|
156
|
-
|
|
259
|
+
Mounted by `monitorRouter`. Every route requires authentication **and** the `superadmin`
|
|
260
|
+
role (`authenticate` + `requireRole('superadmin')` from `@spfn/auth/server`).
|
|
261
|
+
|
|
262
|
+
| Method | Path | Query / body | Description |
|
|
263
|
+
|--------|------|--------------|-------------|
|
|
264
|
+
| GET | `/_monitor/admin/errors` | `status, path, search, dateFrom, dateTo, limit(1-100), offset` | List error groups (limit default 20) |
|
|
265
|
+
| GET | `/_monitor/admin/errors/:id` | — | Group detail + recent 20 events |
|
|
266
|
+
| PATCH | `/_monitor/admin/errors/:id` | body `{ status }` | Update status (resolve/ignore/reopen) |
|
|
267
|
+
| GET | `/_monitor/admin/errors/:id/events` | `limit(1-100), offset` | Events for a group (limit default 20) |
|
|
268
|
+
| GET | `/_monitor/admin/logs` | `level, source, search, requestId, userId, dateFrom, dateTo, limit(1-100), offset` | Query logs (limit default 50) |
|
|
269
|
+
| GET | `/_monitor/admin/stats` | — | `MonitorStats` dashboard aggregate |
|
|
270
|
+
|
|
271
|
+
`:id` and `limit`/`offset` are numbers; `dateFrom`/`dateTo` are ISO strings.
|
|
272
|
+
|
|
273
|
+
### API client
|
|
157
274
|
|
|
158
275
|
```typescript
|
|
159
|
-
|
|
276
|
+
import { monitorApi } from '@spfn/monitor';
|
|
277
|
+
|
|
278
|
+
const stats = await monitorApi.getStats.call({});
|
|
279
|
+
const errors = await monitorApi.listErrors.call({ query: { status: 'active', limit: 20 } });
|
|
280
|
+
const detail = await monitorApi.getErrorDetail.call({ params: { id: 42 } });
|
|
281
|
+
const events = await monitorApi.listErrorEvents.call({ params: { id: 42 }, query: { limit: 50 } });
|
|
282
|
+
const logs = await monitorApi.listLogs.call({ query: { level: 'error' } });
|
|
283
|
+
await monitorApi.updateErrorStatus.call({ params: { id: 42 }, body: { status: 'resolved' } });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`monitorApi` is built with `createApi<typeof monitorRouter>` from `@spfn/core/nextjs`.
|
|
287
|
+
Route keys: `listErrors`, `getErrorDetail`, `updateErrorStatus`, `listErrorEvents`,
|
|
288
|
+
`listLogs`, `getStats`.
|
|
289
|
+
|
|
290
|
+
### `MonitorStats` shape
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
interface MonitorStats
|
|
294
|
+
{
|
|
295
|
+
errors: { total: number; active: number; resolved: number; ignored: number };
|
|
296
|
+
recentErrors: ErrorGroup[]; // latest 10 active groups
|
|
297
|
+
logs: { total: number; byLevel: Record<LogLevel, number> };
|
|
298
|
+
trends: { errorsLast24h: number; errorsLast7d: number; logsLast24h: number };
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Dashboard components
|
|
305
|
+
|
|
306
|
+
`'use client'` React components from `@spfn/monitor/nextjs/client`. They fetch through
|
|
307
|
+
`monitorApi` (so the admin routes must be served and the caller must be a superadmin) and
|
|
308
|
+
style with Tailwind CSS (dark-mode aware).
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// app/admin/monitor/page.tsx
|
|
160
312
|
import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
|
|
161
313
|
|
|
162
|
-
export default function MonitorPage()
|
|
163
|
-
|
|
314
|
+
export default function MonitorPage()
|
|
315
|
+
{
|
|
316
|
+
return <MonitorDashboard />; // no props — tabs (errors/logs) + stats + drill-down
|
|
164
317
|
}
|
|
165
318
|
```
|
|
166
319
|
|
|
167
|
-
|
|
320
|
+
Individual components and their props:
|
|
168
321
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
322
|
+
| Component | Props | Notes |
|
|
323
|
+
|-----------|-------|-------|
|
|
324
|
+
| `MonitorDashboard` | — | Full dashboard: stats + Errors/Logs tabs + detail drill-down |
|
|
325
|
+
| `StatsOverview` | — | Error/log count + trend cards |
|
|
326
|
+
| `ErrorListView` | `onSelect?: (id: number) => void` | Filterable error-group table |
|
|
327
|
+
| `ErrorDetailView` | `errorId: number`, `onBack?: () => void` | Detail + event timeline + status actions |
|
|
328
|
+
| `LogViewer` | — | Searchable log list with expandable metadata |
|
|
174
329
|
|
|
175
|
-
|
|
330
|
+
```tsx
|
|
331
|
+
<ErrorListView onSelect={(id) => router.push(`/admin/monitor/errors/${id}`)} />
|
|
332
|
+
<ErrorDetailView errorId={42} onBack={() => router.back()} />
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Pitfalls & anti-patterns
|
|
338
|
+
|
|
339
|
+
- **`mergeRouters` does not exist.** Some older docs/JSDoc show
|
|
340
|
+
`.routes(mergeRouters(appRouter, monitorRouter))` — `@spfn/core/route` exports no such
|
|
341
|
+
function. Mount the monitor router with `.packages([monitorRouter])` on your app router.
|
|
342
|
+
- **Package routes are excluded from `AppRouter` client types.** After
|
|
343
|
+
`.packages([monitorRouter])`, the `/_monitor/admin/*` routes are served but **not** on the
|
|
344
|
+
`api` client — always call them through `monitorApi`, not `api`.
|
|
345
|
+
- **Don't import `/server` or `/config` into client/edge bundles.** `/server` pulls in
|
|
346
|
+
`node:crypto`, DB access, and (via the config side-effect) the env registry. The only
|
|
347
|
+
browser-safe entry points are `@spfn/monitor` (client API + types) and
|
|
348
|
+
`@spfn/monitor/nextjs/client` (components).
|
|
349
|
+
- **`onError` only fires for thrown/error responses ≥ `minStatusCode` (default 500).** 4xx
|
|
350
|
+
results that don't throw aren't tracked. Lower `minStatusCode` (env or option) to widen.
|
|
351
|
+
- **Fingerprint includes `path`.** The *same* error surfacing on two routes becomes two
|
|
352
|
+
groups; a templated path with embedded ids (`/users/123`) fragments groups — normalize the
|
|
353
|
+
path before it reaches tracking if you need them merged.
|
|
354
|
+
- **No automatic retention/purge.** `*_RETENTION_DAYS` are just config values read by getters;
|
|
355
|
+
nothing deletes old rows on its own. Schedule `logStore.purge()` /
|
|
356
|
+
`logsRepository.deleteOlderThan()` (and an equivalent for error events) yourself.
|
|
357
|
+
- **No Slack webhook ⇒ silent skip, not an error.** `notifyErrorToSlack` logs a warning and
|
|
358
|
+
returns when the URL is unset; tracking still persists to the DB.
|
|
359
|
+
- **`setLogStore` is global and unconditional.** It replaces the store for the whole process
|
|
360
|
+
(including the admin log route). There is no per-call store override.
|
|
361
|
+
- **`userId` is a string everywhere here** (`error_events.user_id`, `logs.user_id`,
|
|
362
|
+
`WriteLogParams.userId`). The error handler stringifies the auth context's `userId` for you;
|
|
363
|
+
in manual calls pass a string.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Complete example
|
|
176
368
|
|
|
177
369
|
```typescript
|
|
178
|
-
|
|
370
|
+
// src/server/router.ts
|
|
371
|
+
import { defineRouter } from '@spfn/core/route';
|
|
372
|
+
import { authRouter } from '@spfn/auth/server';
|
|
373
|
+
import { monitorRouter } from '@spfn/monitor/server';
|
|
374
|
+
|
|
375
|
+
export const appRouter = defineRouter({ /* app routes */ })
|
|
376
|
+
.packages([authRouter, monitorRouter]);
|
|
377
|
+
```
|
|
179
378
|
|
|
180
|
-
|
|
181
|
-
|
|
379
|
+
```typescript
|
|
380
|
+
// src/server/server.config.ts
|
|
381
|
+
import { defineServerConfig } from '@spfn/core/server';
|
|
382
|
+
import { createMonitorErrorHandler, createMonitorLifecycle } from '@spfn/monitor/server';
|
|
383
|
+
import { configureMonitor } from '@spfn/monitor/config';
|
|
384
|
+
import { appRouter } from './router';
|
|
182
385
|
|
|
183
|
-
|
|
184
|
-
const errors = await monitorApi.listErrors.call({
|
|
185
|
-
query: { status: 'active', limit: 20 },
|
|
186
|
-
});
|
|
386
|
+
configureMonitor({ minStatusCode: 500, logRetentionDays: 14 });
|
|
187
387
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
388
|
+
export default defineServerConfig()
|
|
389
|
+
.middleware({
|
|
390
|
+
onError: createMonitorErrorHandler({ environment: process.env.NODE_ENV }),
|
|
391
|
+
})
|
|
392
|
+
.routes(appRouter)
|
|
393
|
+
.lifecycle(createMonitorLifecycle())
|
|
394
|
+
.build();
|
|
193
395
|
```
|
|
194
396
|
|
|
195
|
-
|
|
397
|
+
```tsx
|
|
398
|
+
// app/admin/monitor/page.tsx — superadmin-gated route
|
|
399
|
+
import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
|
|
400
|
+
|
|
401
|
+
export default function MonitorPage()
|
|
402
|
+
{
|
|
403
|
+
return <MonitorDashboard />;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
196
406
|
|
|
197
407
|
```typescript
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
export type { MonitorRouter, MonitorStats, ErrorGroupStatus, LogLevel };
|
|
201
|
-
|
|
202
|
-
// From '@spfn/monitor/server'
|
|
203
|
-
export {
|
|
204
|
-
// Integration
|
|
205
|
-
monitorRouter,
|
|
206
|
-
createMonitorErrorHandler,
|
|
207
|
-
createMonitorLifecycle,
|
|
408
|
+
// anywhere server-side: structured logging
|
|
409
|
+
import { monitor } from '@spfn/monitor/server';
|
|
208
410
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
writeLog, queryLogs,
|
|
212
|
-
getMonitorStats,
|
|
213
|
-
setLogStore,
|
|
411
|
+
await monitor.log({ level: 'info', message: 'job done', source: 'cron', metadata: { took: 120 } });
|
|
412
|
+
```
|
|
214
413
|
|
|
215
|
-
|
|
216
|
-
errorGroups, errorEvents, logs,
|
|
217
|
-
errorGroupsRepository, errorEventsRepository, logsRepository,
|
|
218
|
-
};
|
|
414
|
+
---
|
|
219
415
|
|
|
220
|
-
|
|
221
|
-
export { configureMonitor };
|
|
416
|
+
## Exports reference
|
|
222
417
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
418
|
+
```typescript
|
|
419
|
+
// '@spfn/monitor' (client-safe)
|
|
420
|
+
monitorApi
|
|
421
|
+
type MonitorRouter, MonitorStats, ErrorGroupStatus, LogLevel
|
|
422
|
+
ERROR_GROUP_STATUSES, LOG_LEVELS
|
|
423
|
+
|
|
424
|
+
// '@spfn/monitor/server'
|
|
425
|
+
monitorRouter
|
|
426
|
+
createMonitorErrorHandler, createMonitorLifecycle // integration
|
|
427
|
+
trackError, updateErrorGroupStatus, generateFingerprint // error tracking
|
|
428
|
+
writeLog, queryLogs, setLogStore, getLogStore, monitor // logging
|
|
429
|
+
getMonitorStats // stats
|
|
430
|
+
errorGroupsRepository, errorEventsRepository, logsRepository // repositories
|
|
431
|
+
errorGroups, errorEvents, logs, monitorSchema // entities
|
|
432
|
+
ERROR_GROUP_STATUSES, LOG_LEVELS, monitorLogger
|
|
433
|
+
type ErrorTrackingContext, MonitorErrorHandlerOptions, MonitorLifecycleConfig,
|
|
434
|
+
LogStore, WriteLogParams, MonitorStats, ErrorGroupFilters, LogFilters,
|
|
435
|
+
ErrorGroup, NewErrorGroup, ErrorGroupStatus, ErrorEvent, NewErrorEvent,
|
|
436
|
+
Log, NewLog, LogLevel
|
|
437
|
+
|
|
438
|
+
// '@spfn/monitor/config'
|
|
439
|
+
configureMonitor, getMonitorConfig
|
|
440
|
+
getSlackWebhookUrl, getErrorRetentionDays, getLogRetentionDays, getMinStatusCode
|
|
441
|
+
env, monitorEnvSchema
|
|
442
|
+
type MonitorConfig
|
|
443
|
+
|
|
444
|
+
// '@spfn/monitor/nextjs/client'
|
|
445
|
+
MonitorDashboard, StatsOverview, ErrorListView, ErrorDetailView, LogViewer
|
|
228
446
|
```
|
|
229
447
|
|
|
230
|
-
##
|
|
448
|
+
## Related
|
|
231
449
|
|
|
232
|
-
|
|
450
|
+
- [@spfn/core/route](../core/src/route/README.md) — `defineRouter`, `.packages()`, route DSL
|
|
451
|
+
- [@spfn/core/server](../core/src/server/README.md) — `defineServerConfig`, `onError` middleware
|
|
452
|
+
- [@spfn/core/env](../core/src/env/README.md) — env schema / registry used by `/config`
|
|
453
|
+
- [@spfn/notification](../notification/README.md) — `sendSlack`, `createErrorSlackNotifier`
|
|
454
|
+
- [@spfn/auth](../auth/README.md) — `authenticate`, `requireRole` guarding the admin routes
|