@spfn/monitor 0.1.0-beta.17 → 0.1.0-beta.18

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/README.md CHANGED
@@ -1,28 +1,54 @@
1
- # @spfn/monitor
1
+ # @spfn/monitor — DB-backed error tracking, logging, and admin dashboard
2
2
 
3
- Error tracking, log management, and monitoring dashboard for SPFN applications.
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
- ## Features
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
- ## Quick Start
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
- ### Server Configuration
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 { createMonitorErrorHandler, createMonitorLifecycle, monitorRouter } from '@spfn/monitor/server';
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
- Register the monitor router as a package router:
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
- ### Database Migration
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 migrate
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
- This creates the `spfn_monitor` schema with `error_groups`, `error_events`, and `logs` tables.
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 Variables
94
+ ### Environment variables
54
95
 
55
- ```bash
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
- # Error retention in days (default: 90)
60
- SPFN_MONITOR_ERROR_RETENTION_DAYS=90
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
- # Log retention in days (default: 30)
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
- # Minimum HTTP status code to track (default: 500)
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: 'https://hooks.slack.com/services/...',
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
- ## Error Tracking
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
- Errors are automatically tracked when using `createMonitorErrorHandler()`:
130
+ ---
85
131
 
86
- 1. **New error** - Creates error group + event, sends Slack notification
87
- 2. **Repeated error** (active/ignored) - Increments count, records event, no notification
88
- 3. **Reopened error** (was resolved) - Changes status to active, sends Slack notification
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
- Errors are grouped by a SHA-256 fingerprint of `name:message:path`, producing a 16-character hex ID.
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 Error Tracking
166
+ ### Manual tracking
95
167
 
96
168
  ```typescript
97
169
  import { trackError } from '@spfn/monitor/server';
98
170
 
99
- await trackError(error, {
100
- statusCode: 500,
101
- path: '/api/example',
102
- method: 'POST',
103
- requestId: 'req_123',
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
- ## Developer Logging
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
- userId: 'user_123',
118
- metadata: { provider: 'google' },
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
- // Query logs
122
- const logs = await queryLogs({
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
- limit: 50,
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
- ### Custom Log Store
232
+ `writeLog` / `queryLogs` return `Log` / `Log[]`. `LOG_LEVELS` =
233
+ `['debug','info','warn','error','fatal']` (type `LogLevel`).
234
+
235
+ ### Custom log store
130
236
 
131
- Replace the default DB storage with a custom implementation:
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
- setLogStore({
137
- async write(entry) { /* S3, ClickHouse, etc. */ },
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
- ## Admin API Routes
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
- All routes require `superadmin` role authentication.
255
+ ---
146
256
 
147
- | Method | Path | Description |
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
- ## Dashboard Components
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
- // In your Next.js page
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
- return <MonitorDashboard />;
314
+ export default function MonitorPage()
315
+ {
316
+ return <MonitorDashboard />; // no props — tabs (errors/logs) + stats + drill-down
164
317
  }
165
318
  ```
166
319
 
167
- Available components:
320
+ Individual components and their props:
168
321
 
169
- - `MonitorDashboard` - Full dashboard with tabs (errors, logs) and stats
170
- - `StatsOverview` - Error/log count cards with trends
171
- - `ErrorListView` - Filterable error group table
172
- - `ErrorDetailView` - Error detail with event timeline and status actions
173
- - `LogViewer` - Searchable log list with expandable metadata
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
- ## API Client
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
- import { monitorApi } from '@spfn/monitor';
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
- // Get dashboard stats
181
- const stats = await monitorApi.getStats.call({});
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
- // List active errors
184
- const errors = await monitorApi.listErrors.call({
185
- query: { status: 'active', limit: 20 },
186
- });
386
+ configureMonitor({ minStatusCode: 500, logRetentionDays: 14 });
187
387
 
188
- // Resolve an error
189
- await monitorApi.updateErrorStatus.call({
190
- params: { id: 1 },
191
- body: { status: 'resolved' },
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
- ## Exports
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
- // From '@spfn/monitor'
199
- export { monitorApi };
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
- // Services
210
- trackError, updateErrorGroupStatus,
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
- // Entities & Repositories
216
- errorGroups, errorEvents, logs,
217
- errorGroupsRepository, errorEventsRepository, logsRepository,
218
- };
414
+ ---
219
415
 
220
- // From '@spfn/monitor/config'
221
- export { configureMonitor };
416
+ ## Exports reference
222
417
 
223
- // From '@spfn/monitor/nextjs/client'
224
- export {
225
- MonitorDashboard, StatsOverview,
226
- ErrorListView, ErrorDetailView, LogViewer,
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
- ## License
448
+ ## Related
231
449
 
232
- MIT
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
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as _spfn_core_nextjs from '@spfn/core/nextjs';
2
2
  import * as _spfn_core_route from '@spfn/core/route';
3
3
  import * as _sinclair_typebox from '@sinclair/typebox';
4
- import { M as MonitorStats, m as monitorRouter } from './index-BiN0PoSx.js';
5
- export { a as ERROR_GROUP_STATUSES, E as ErrorGroupStatus, b as LOG_LEVELS, L as LogLevel } from './index-BiN0PoSx.js';
4
+ import { M as MonitorStats, m as monitorRouter } from './index-C9IUDNIv.js';
5
+ export { a as ERROR_GROUP_STATUSES, E as ErrorGroupStatus, b as LOG_LEVELS, L as LogLevel } from './index-C9IUDNIv.js';
6
6
  import 'drizzle-orm/pg-core';
7
7
 
8
8
  /**
@@ -33,14 +33,14 @@ declare const monitorApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
33
33
  offset: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
34
34
  }>;
35
35
  }, {}, {
36
+ status: "active" | "resolved" | "ignored";
37
+ path: string;
36
38
  id: number;
37
39
  fingerprint: string;
38
40
  name: string;
39
41
  message: string;
40
- path: string;
41
42
  method: string;
42
43
  statusCode: number;
43
- status: "active" | "resolved" | "ignored";
44
44
  count: number;
45
45
  firstSeenAt: Date;
46
46
  lastSeenAt: Date;
@@ -54,14 +54,14 @@ declare const monitorApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
54
54
  }>;
55
55
  }, {}, {
56
56
  group: {
57
+ status: "active" | "resolved" | "ignored";
58
+ path: string;
57
59
  id: number;
58
60
  fingerprint: string;
59
61
  name: string;
60
62
  message: string;
61
- path: string;
62
63
  method: string;
63
64
  statusCode: number;
64
- status: "active" | "resolved" | "ignored";
65
65
  count: number;
66
66
  firstSeenAt: Date;
67
67
  lastSeenAt: Date;
@@ -70,15 +70,15 @@ declare const monitorApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
70
70
  updatedAt: Date;
71
71
  };
72
72
  events: {
73
+ query: Record<string, string> | null;
73
74
  id: number;
74
75
  statusCode: number;
75
76
  createdAt: Date;
76
77
  updatedAt: Date;
77
- query: Record<string, string> | null;
78
+ headers: Record<string, string> | null;
78
79
  groupId: number;
79
80
  requestId: string | null;
80
81
  userId: string | null;
81
- headers: Record<string, string> | null;
82
82
  stackTrace: string | null;
83
83
  metadata: Record<string, unknown> | null;
84
84
  }[];
@@ -91,14 +91,14 @@ declare const monitorApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
91
91
  status: _sinclair_typebox.TString;
92
92
  }>;
93
93
  }, {}, {
94
+ status: "active" | "resolved" | "ignored";
95
+ path: string;
94
96
  id: number;
95
97
  fingerprint: string;
96
98
  name: string;
97
99
  message: string;
98
- path: string;
99
100
  method: string;
100
101
  statusCode: number;
101
- status: "active" | "resolved" | "ignored";
102
102
  count: number;
103
103
  firstSeenAt: Date;
104
104
  lastSeenAt: Date;
@@ -115,15 +115,15 @@ declare const monitorApi: _spfn_core_nextjs.Client<_spfn_core_route.Router<{
115
115
  offset: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
116
116
  }>;
117
117
  }, {}, {
118
+ query: Record<string, string> | null;
118
119
  id: number;
119
120
  statusCode: number;
120
121
  createdAt: Date;
121
122
  updatedAt: Date;
122
- query: Record<string, string> | null;
123
+ headers: Record<string, string> | null;
123
124
  groupId: number;
124
125
  requestId: string | null;
125
126
  userId: string | null;
126
- headers: Record<string, string> | null;
127
127
  stackTrace: string | null;
128
128
  metadata: Record<string, unknown> | null;
129
129
  }[]>;
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as ErrorGroupStatus, c as ErrorGroup, N as NewErrorGroup, L as LogLevel, d as NewLog, e as Log } from './index-BiN0PoSx.js';
2
- export { a as ERROR_GROUP_STATUSES, b as LOG_LEVELS, M as MonitorStats, f as errorGroups, g as getMonitorStats, l as logs, m as monitorRouter } from './index-BiN0PoSx.js';
1
+ import { E as ErrorGroupStatus, c as ErrorGroup, N as NewErrorGroup, L as LogLevel, d as NewLog, e as Log } from './index-C9IUDNIv.js';
2
+ export { a as ERROR_GROUP_STATUSES, b as LOG_LEVELS, M as MonitorStats, f as errorGroups, g as getMonitorStats, l as logs, m as monitorRouter } from './index-C9IUDNIv.js';
3
3
  import { BaseRepository } from '@spfn/core/db';
4
4
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
5
5
  import * as _spfn_core_logger from '@spfn/core/logger';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/monitor",
3
- "version": "0.1.0-beta.17",
3
+ "version": "0.1.0-beta.18",
4
4
  "type": "module",
5
5
  "description": "Error tracking, log management, and monitoring dashboard for SPFN",
6
6
  "main": "./dist/index.js",
@@ -58,24 +58,25 @@
58
58
  "dir": "./migrations"
59
59
  }
60
60
  },
61
- "dependencies": {
62
- "@spfn/auth": "0.2.0-beta.57",
63
- "@spfn/core": "0.2.0-beta.40",
64
- "@spfn/notification": "0.1.0-beta.17"
65
- },
66
61
  "devDependencies": {
67
62
  "@sinclair/typebox": "^0.34.30",
68
63
  "@types/node": "^20.11.0",
69
64
  "@types/react": "^19.2.2",
70
65
  "@types/react-dom": "^19.2.2",
71
66
  "drizzle-kit": "^0.31.6",
72
- "drizzle-orm": "^0.44.2",
67
+ "drizzle-orm": "^0.45.0",
73
68
  "madge": "^8.0.0",
74
69
  "tsup": "^8.5.0",
75
70
  "typescript": "^5.3.3",
76
- "vitest": "^4.0.6"
71
+ "vitest": "^4.0.6",
72
+ "@spfn/auth": "0.2.0-beta.66",
73
+ "@spfn/core": "0.2.0-beta.49",
74
+ "@spfn/notification": "0.1.0-beta.19"
77
75
  },
78
76
  "peerDependencies": {
77
+ "@spfn/auth": ">=0.2.0-beta.66",
78
+ "@spfn/core": ">=0.2.0-beta.49",
79
+ "@spfn/notification": ">=0.1.0-beta.18",
79
80
  "next": "^15.0.0 || ^16.0.0"
80
81
  },
81
82
  "peerDependenciesMeta": {
@@ -497,14 +497,14 @@ declare const monitorRouter: _spfn_core_route.Router<{
497
497
  offset: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
498
498
  }>;
499
499
  }, {}, {
500
+ status: "active" | "resolved" | "ignored";
501
+ path: string;
500
502
  id: number;
501
503
  fingerprint: string;
502
504
  name: string;
503
505
  message: string;
504
- path: string;
505
506
  method: string;
506
507
  statusCode: number;
507
- status: "active" | "resolved" | "ignored";
508
508
  count: number;
509
509
  firstSeenAt: Date;
510
510
  lastSeenAt: Date;
@@ -518,14 +518,14 @@ declare const monitorRouter: _spfn_core_route.Router<{
518
518
  }>;
519
519
  }, {}, {
520
520
  group: {
521
+ status: "active" | "resolved" | "ignored";
522
+ path: string;
521
523
  id: number;
522
524
  fingerprint: string;
523
525
  name: string;
524
526
  message: string;
525
- path: string;
526
527
  method: string;
527
528
  statusCode: number;
528
- status: "active" | "resolved" | "ignored";
529
529
  count: number;
530
530
  firstSeenAt: Date;
531
531
  lastSeenAt: Date;
@@ -534,15 +534,15 @@ declare const monitorRouter: _spfn_core_route.Router<{
534
534
  updatedAt: Date;
535
535
  };
536
536
  events: {
537
+ query: Record<string, string> | null;
537
538
  id: number;
538
539
  statusCode: number;
539
540
  createdAt: Date;
540
541
  updatedAt: Date;
541
- query: Record<string, string> | null;
542
+ headers: Record<string, string> | null;
542
543
  groupId: number;
543
544
  requestId: string | null;
544
545
  userId: string | null;
545
- headers: Record<string, string> | null;
546
546
  stackTrace: string | null;
547
547
  metadata: Record<string, unknown> | null;
548
548
  }[];
@@ -555,14 +555,14 @@ declare const monitorRouter: _spfn_core_route.Router<{
555
555
  status: _sinclair_typebox.TString;
556
556
  }>;
557
557
  }, {}, {
558
+ status: "active" | "resolved" | "ignored";
559
+ path: string;
558
560
  id: number;
559
561
  fingerprint: string;
560
562
  name: string;
561
563
  message: string;
562
- path: string;
563
564
  method: string;
564
565
  statusCode: number;
565
- status: "active" | "resolved" | "ignored";
566
566
  count: number;
567
567
  firstSeenAt: Date;
568
568
  lastSeenAt: Date;
@@ -579,15 +579,15 @@ declare const monitorRouter: _spfn_core_route.Router<{
579
579
  offset: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
580
580
  }>;
581
581
  }, {}, {
582
+ query: Record<string, string> | null;
582
583
  id: number;
583
584
  statusCode: number;
584
585
  createdAt: Date;
585
586
  updatedAt: Date;
586
- query: Record<string, string> | null;
587
+ headers: Record<string, string> | null;
587
588
  groupId: number;
588
589
  requestId: string | null;
589
590
  userId: string | null;
590
- headers: Record<string, string> | null;
591
591
  stackTrace: string | null;
592
592
  metadata: Record<string, unknown> | null;
593
593
  }[]>;