api-observe 1.1.2 → 1.1.3
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 +133 -2
- package/lib/adapters/express.js +14 -18
- package/lib/adapters/http.js +20 -17
- package/lib/failure-store.js +38 -1
- package/lib/plugin.js +2 -5
- package/lib/router.js +6 -6
- package/lib/routes.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,12 @@ Works with **Fastify**, **Express**, **NestJS**, **Koa**, or **plain Node.js HTT
|
|
|
8
8
|
|
|
9
9
|
- Captures upstream API failures (via Axios interceptors)
|
|
10
10
|
- Captures controller/route-level errors (via framework hooks)
|
|
11
|
+
- **Failure records are keyed to the inbound request** — when `/parties` calls `/api/aggregate` and it fails, the log entry shows `/parties` at the top level with the full `/api/aggregate` failure nested inside
|
|
11
12
|
- Built-in HTML dashboard at `/observe`
|
|
12
13
|
- REST API for querying failures programmatically
|
|
13
14
|
- In-memory ring buffer with configurable size and TTL
|
|
15
|
+
- **`onCapture` hook** — persist failures to your own database alongside the in-memory store
|
|
16
|
+
- **Custom store** — replace the in-memory store entirely with a database-backed implementation
|
|
14
17
|
- Automatic redaction of sensitive headers (authorization, tokens, passwords)
|
|
15
18
|
- Zero external dependencies
|
|
16
19
|
|
|
@@ -55,6 +58,15 @@ The Fastify plugin automatically captures:
|
|
|
55
58
|
- **Controller errors** - any error thrown during the request lifecycle (via `onError` hook)
|
|
56
59
|
- **404s** - requests to non-existent routes (via `onResponse` hook)
|
|
57
60
|
|
|
61
|
+
#### How upstream failures are recorded
|
|
62
|
+
|
|
63
|
+
When an inbound request (e.g. `POST /parties`) triggers a downstream call that fails (e.g. `GET /api/aggregate` returns 500), the failure record is keyed to the **inbound** route — not the downstream URL. Opening the record in the dashboard shows:
|
|
64
|
+
|
|
65
|
+
- **Top level**: `POST /parties` — the service, method, URL, correlation ID, and inbound request headers/body
|
|
66
|
+
- **Upstream Call** section: the downstream service name, `GET /api/aggregate`, status code, duration, error message, outgoing request headers/body, and the downstream response
|
|
67
|
+
|
|
68
|
+
This lets you immediately answer "which of my endpoints is broken?" rather than having to trace a downstream URL back to the calling route.
|
|
69
|
+
|
|
58
70
|
---
|
|
59
71
|
|
|
60
72
|
### Express
|
|
@@ -183,9 +195,12 @@ bootstrap();
|
|
|
183
195
|
Visit `/observe` in your browser to access the built-in dashboard.
|
|
184
196
|
|
|
185
197
|
The dashboard provides:
|
|
186
|
-
- Real-time failure table with filtering by type, service, method, status code, and
|
|
198
|
+
- Real-time failure table with filtering by type, service, method, status code, URL, and correlation ID
|
|
187
199
|
- Summary cards showing total failures, upstream vs controller errors, and affected services
|
|
188
|
-
- Detail modal with
|
|
200
|
+
- Detail modal with:
|
|
201
|
+
- **Inbound request** headers, query params, and body (the route your app received)
|
|
202
|
+
- **Upstream Call** section — downstream service name, URL, status, duration, outgoing request, and downstream response headers/body (only shown for upstream failures)
|
|
203
|
+
- Stack trace (for controller errors)
|
|
189
204
|
- Auto-refresh every 30 seconds
|
|
190
205
|
- Pagination for large result sets
|
|
191
206
|
|
|
@@ -210,6 +225,7 @@ All endpoints are served under `/observe/api/`:
|
|
|
210
225
|
| `method` | string | Filter by HTTP method (`GET`, `POST`, etc.) |
|
|
211
226
|
| `statusCode` | number | Filter by response status code |
|
|
212
227
|
| `url` | string | Filter by URL (partial match) |
|
|
228
|
+
| `correlationId` | string | Filter by correlation ID (partial match) |
|
|
213
229
|
| `from` | string | Start date (ISO 8601) |
|
|
214
230
|
| `to` | string | End date (ISO 8601) |
|
|
215
231
|
| `limit` | number | Results per page (default: 50) |
|
|
@@ -221,6 +237,9 @@ All endpoints are served under `/observe/api/`:
|
|
|
221
237
|
# Get all 5xx failures from the auth service
|
|
222
238
|
curl "http://localhost:3000/observe/api/failures?service=auth&statusCode=500"
|
|
223
239
|
|
|
240
|
+
# Get failures by correlation ID
|
|
241
|
+
curl "http://localhost:3000/observe/api/failures?correlationId=abc-123"
|
|
242
|
+
|
|
224
243
|
# Get summary stats
|
|
225
244
|
curl "http://localhost:3000/observe/api/summary"
|
|
226
245
|
```
|
|
@@ -234,6 +253,8 @@ All adapters accept the same options:
|
|
|
234
253
|
| `maxEntries` | number | `500` | Maximum failure records to keep in memory |
|
|
235
254
|
| `ttlMs` | number | `86400000` (24h) | Time-to-live in ms before entries are evicted |
|
|
236
255
|
| `sanitize` | function | built-in | Custom sanitizer for request/response bodies |
|
|
256
|
+
| `onCapture` | function | — | Called with each captured record; use to persist to a database |
|
|
257
|
+
| `store` | object | — | Replace the in-memory store with a custom implementation (see [Persisting Failures](#persisting-failures-to-a-database)) |
|
|
237
258
|
|
|
238
259
|
### Default Sanitization
|
|
239
260
|
|
|
@@ -369,6 +390,116 @@ observe.attachToAll(services);
|
|
|
369
390
|
- Direct `BaseHttpClient` instances (has `_axios` + `serviceName`)
|
|
370
391
|
- Service wrappers with a `.client` property that is a `BaseHttpClient`
|
|
371
392
|
|
|
393
|
+
## Persisting Failures to a Database
|
|
394
|
+
|
|
395
|
+
By default `api-observe` keeps failures in an in-memory ring buffer. Two options let you persist them externally.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
### Option A — `onCapture` callback (recommended for most apps)
|
|
400
|
+
|
|
401
|
+
Pass an `onCapture` function. It is called with every captured record immediately after it is stored in memory. Use it to write to your own database while still benefiting from the built-in dashboard and in-memory query API.
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
// Fastify
|
|
405
|
+
await fastify.register(apiObserve, {
|
|
406
|
+
onCapture: async (record) => {
|
|
407
|
+
await db.collection('api_failures').insertOne(record);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Express
|
|
412
|
+
const observe = expressMiddleware({
|
|
413
|
+
onCapture: async (record) => {
|
|
414
|
+
await db.query(
|
|
415
|
+
'INSERT INTO api_failures(data) VALUES($1)',
|
|
416
|
+
[JSON.stringify(record)],
|
|
417
|
+
);
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`record` is the fully-formed failure object (same shape as returned by `GET /observe/api/failures/:id`). Errors thrown inside `onCapture` are silently swallowed so a failing hook never disrupts your application.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
### Option B — Custom store (full replacement)
|
|
427
|
+
|
|
428
|
+
Pass your own `store` object to replace the in-memory store entirely. The dashboard and REST API will query it, so failures are served from your database.
|
|
429
|
+
|
|
430
|
+
Your store must implement this interface:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
interface FailureStore {
|
|
434
|
+
add(failure: object): object; // store a record, return it with id + timestamp
|
|
435
|
+
query(filters: object): { data: object[], total: number, limit: number, offset: number };
|
|
436
|
+
getById(id: number | string): object | null;
|
|
437
|
+
summary(): object;
|
|
438
|
+
clear(): void;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
#### Example — MongoDB-backed store
|
|
443
|
+
|
|
444
|
+
```js
|
|
445
|
+
class MongoFailureStore {
|
|
446
|
+
constructor(collection) {
|
|
447
|
+
this._col = collection;
|
|
448
|
+
this._seq = 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async add(failure) {
|
|
452
|
+
const record = { id: ++this._seq, timestamp: new Date().toISOString(), ...failure };
|
|
453
|
+
await this._col.insertOne(record);
|
|
454
|
+
return record;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async query({ service, statusCode, method, url, correlationId, type, limit = 50, offset = 0 } = {}) {
|
|
458
|
+
const filter = {};
|
|
459
|
+
if (type) filter.type = type;
|
|
460
|
+
if (service) filter.service = new RegExp(service, 'i');
|
|
461
|
+
if (method) filter.method = method.toUpperCase();
|
|
462
|
+
if (statusCode) filter.statusCode = Number(statusCode);
|
|
463
|
+
if (url) filter.url = new RegExp(url, 'i');
|
|
464
|
+
if (correlationId) filter.correlationId = new RegExp(correlationId, 'i');
|
|
465
|
+
|
|
466
|
+
const total = await this._col.countDocuments(filter);
|
|
467
|
+
const data = await this._col.find(filter).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray();
|
|
468
|
+
return { data, total, limit, offset };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async getById(id) {
|
|
472
|
+
return this._col.findOne({ id: Number(id) }) ?? null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async summary() {
|
|
476
|
+
const all = await this._col.find({}).toArray();
|
|
477
|
+
const byService = {}, byStatusCode = {}, byType = {};
|
|
478
|
+
for (const e of all) {
|
|
479
|
+
byService[e.service || 'unknown'] = (byService[e.service || 'unknown'] || 0) + 1;
|
|
480
|
+
const code = e.statusCode || 'unknown';
|
|
481
|
+
byStatusCode[code] = (byStatusCode[code] || 0) + 1;
|
|
482
|
+
const type = e.type || 'upstream';
|
|
483
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
484
|
+
}
|
|
485
|
+
return { totalFailures: all.length, byService, byStatusCode, byType };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async clear() {
|
|
489
|
+
await this._col.deleteMany({});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Pass it to the adapter
|
|
494
|
+
const observe = expressMiddleware({
|
|
495
|
+
store: new MongoFailureStore(db.collection('api_failures')),
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
> **Note:** When using a custom async store, your `query` / `getById` / `summary` methods may return Promises — the built-in router will await them automatically.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
372
503
|
## Exports
|
|
373
504
|
|
|
374
505
|
```js
|
package/lib/adapters/express.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
const { parse: parseUrl } = require('url');
|
|
30
|
-
const { FailureStore } = require('../failure-store');
|
|
30
|
+
const { FailureStore, resolveStore } = require('../failure-store');
|
|
31
31
|
const { attachInterceptor, attachToAll, defaultSanitize } = require('../interceptor');
|
|
32
32
|
const { createRouter } = require('../router');
|
|
33
33
|
const { requestContext } = require('../request-context');
|
|
@@ -39,10 +39,7 @@ const { requestContext } = require('../request-context');
|
|
|
39
39
|
* @returns {function} Express middleware with .attach(), .attachToAll(), .store, .errorHandler()
|
|
40
40
|
*/
|
|
41
41
|
function expressMiddleware(opts = {}) {
|
|
42
|
-
const store =
|
|
43
|
-
maxEntries: opts.maxEntries,
|
|
44
|
-
ttlMs: opts.ttlMs,
|
|
45
|
-
});
|
|
42
|
+
const store = resolveStore(opts);
|
|
46
43
|
|
|
47
44
|
const sanitize = opts.sanitize ?? defaultSanitize;
|
|
48
45
|
const sanitizeOpts = opts.sanitize ? { sanitize: opts.sanitize } : {};
|
|
@@ -59,19 +56,18 @@ function expressMiddleware(opts = {}) {
|
|
|
59
56
|
return;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
res.end(body);
|
|
59
|
+
router(req.method, pathname, parsed.query).then(result => {
|
|
60
|
+
if (!result) {
|
|
61
|
+
next();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res.statusCode = result.status;
|
|
65
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
66
|
+
res.setHeader(key, value);
|
|
67
|
+
}
|
|
68
|
+
const body = typeof result.body === 'string' ? result.body : JSON.stringify(result.body);
|
|
69
|
+
res.end(body);
|
|
70
|
+
}).catch(next);
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
// ── Error-capturing middleware (use AFTER your routes) ───────────────────
|
package/lib/adapters/http.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
const { parse: parseUrl } = require('url');
|
|
37
|
-
const { FailureStore } = require('../failure-store');
|
|
37
|
+
const { FailureStore, resolveStore } = require('../failure-store');
|
|
38
38
|
const { attachInterceptor, attachToAll, defaultSanitize } = require('../interceptor');
|
|
39
39
|
const { createRouter } = require('../router');
|
|
40
40
|
|
|
@@ -45,10 +45,7 @@ const { createRouter } = require('../router');
|
|
|
45
45
|
* @returns {object} Handler with .handle(), .attach(), .attachToAll(), .captureError(), .store
|
|
46
46
|
*/
|
|
47
47
|
function createHttpHandler(opts = {}) {
|
|
48
|
-
const store =
|
|
49
|
-
maxEntries: opts.maxEntries,
|
|
50
|
-
ttlMs: opts.ttlMs,
|
|
51
|
-
});
|
|
48
|
+
const store = resolveStore(opts);
|
|
52
49
|
|
|
53
50
|
const sanitize = opts.sanitize ?? defaultSanitize;
|
|
54
51
|
const sanitizeOpts = opts.sanitize ? { sanitize: opts.sanitize } : {};
|
|
@@ -70,18 +67,24 @@ function createHttpHandler(opts = {}) {
|
|
|
70
67
|
return false;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
// routeHandler is async (supports custom async stores); signal handled
|
|
71
|
+
// synchronously by returning true, then resolve asynchronously.
|
|
72
|
+
router(req.method, pathname, parsed.query).then(result => {
|
|
73
|
+
if (!result) {
|
|
74
|
+
res.statusCode = 404;
|
|
75
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
res.statusCode = result.status;
|
|
79
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
80
|
+
res.setHeader(key, value);
|
|
81
|
+
}
|
|
82
|
+
const body = typeof result.body === 'string' ? result.body : JSON.stringify(result.body);
|
|
83
|
+
res.end(body);
|
|
84
|
+
}).catch(err => {
|
|
85
|
+
res.statusCode = 500;
|
|
86
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
87
|
+
});
|
|
85
88
|
return true;
|
|
86
89
|
}
|
|
87
90
|
|
package/lib/failure-store.js
CHANGED
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
class FailureStore {
|
|
15
15
|
/**
|
|
16
|
-
* @param {{ maxEntries?: number, ttlMs?: number }} [opts]
|
|
16
|
+
* @param {{ maxEntries?: number, ttlMs?: number, onCapture?: (record: object) => void }} [opts]
|
|
17
17
|
*/
|
|
18
18
|
constructor(opts = {}) {
|
|
19
19
|
this._maxEntries = opts.maxEntries ?? 500;
|
|
20
20
|
this._ttlMs = opts.ttlMs ?? 24 * 60 * 60 * 1000; // 24 hours
|
|
21
21
|
this._entries = [];
|
|
22
22
|
this._idCounter = 0;
|
|
23
|
+
this._onCapture = typeof opts.onCapture === 'function' ? opts.onCapture : null;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -41,6 +42,12 @@ class FailureStore {
|
|
|
41
42
|
this._entries = this._entries.slice(-this._maxEntries);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// Fire the capture hook (e.g. persist to an external database).
|
|
46
|
+
// Errors are swallowed so a failing hook never disrupts the app.
|
|
47
|
+
if (this._onCapture) {
|
|
48
|
+
try { this._onCapture(record); } catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
return record;
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -161,3 +168,33 @@ class FailureStore {
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
module.exports = { FailureStore };
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a store from adapter options.
|
|
174
|
+
*
|
|
175
|
+
* - If `opts.store` is provided it is used as-is (custom/DB-backed store).
|
|
176
|
+
* It must implement: add(failure), query(filters), getById(id), summary(), clear().
|
|
177
|
+
* - Otherwise a new FailureStore is created, optionally with an onCapture hook.
|
|
178
|
+
*
|
|
179
|
+
* @param {{ store?: object, maxEntries?: number, ttlMs?: number, onCapture?: function }} opts
|
|
180
|
+
* @returns {FailureStore|object}
|
|
181
|
+
*/
|
|
182
|
+
function resolveStore(opts = {}) {
|
|
183
|
+
if (opts.store) {
|
|
184
|
+
const required = ['add', 'query', 'getById', 'summary', 'clear'];
|
|
185
|
+
for (const method of required) {
|
|
186
|
+
if (typeof opts.store[method] !== 'function') {
|
|
187
|
+
throw new Error(`api-observe: custom store is missing required method "${method}"`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return opts.store;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return new FailureStore({
|
|
194
|
+
maxEntries: opts.maxEntries,
|
|
195
|
+
ttlMs: opts.ttlMs,
|
|
196
|
+
onCapture: opts.onCapture,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { FailureStore, resolveStore };
|
package/lib/plugin.js
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* recording a duplicate when the same request has already been captured.
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
const { FailureStore } = require('./failure-store');
|
|
53
|
+
const { FailureStore, resolveStore } = require('./failure-store');
|
|
54
54
|
const { attachInterceptor, attachToAll, defaultSanitize } = require('./interceptor');
|
|
55
55
|
const { registerRoutes } = require('./routes');
|
|
56
56
|
const { requestContext } = require('./request-context');
|
|
@@ -58,10 +58,7 @@ const { requestContext } = require('./request-context');
|
|
|
58
58
|
const CTX_KEY = Symbol.for('ps-observe.ctx');
|
|
59
59
|
|
|
60
60
|
async function observerPlugin(fastify, opts) {
|
|
61
|
-
const store =
|
|
62
|
-
maxEntries: opts.maxEntries,
|
|
63
|
-
ttlMs: opts.ttlMs,
|
|
64
|
-
});
|
|
61
|
+
const store = resolveStore(opts);
|
|
65
62
|
|
|
66
63
|
const sanitizeOpts = opts.sanitize ? { sanitize: opts.sanitize } : {};
|
|
67
64
|
const sanitize = opts.sanitize ?? defaultSanitize;
|
package/lib/router.js
CHANGED
|
@@ -25,9 +25,9 @@ function createRouter(store) {
|
|
|
25
25
|
* @param {string} pathname - URL path, e.g. "/observe/api/failures/3"
|
|
26
26
|
* @param {object} query - Parsed query parameters
|
|
27
27
|
* @returns {{ status: number, headers: object, body: any } | null}
|
|
28
|
-
* null means "no matching route"
|
|
28
|
+
* null means "no matching route" — may be a Promise for async stores
|
|
29
29
|
*/
|
|
30
|
-
return function routeHandler(method, pathname, query) {
|
|
30
|
+
return async function routeHandler(method, pathname, query) {
|
|
31
31
|
// Normalise: strip trailing slash (except root "/observe")
|
|
32
32
|
const path = pathname.replace(/\/$/, '') || '/';
|
|
33
33
|
|
|
@@ -43,14 +43,14 @@ function createRouter(store) {
|
|
|
43
43
|
// ── List failures ──────────────────────────────────────────────────────
|
|
44
44
|
if (method === 'GET' && path === '/observe/api/failures') {
|
|
45
45
|
const { service, statusCode, method: m, url, from, to, limit, offset, type, correlationId } = query || {};
|
|
46
|
-
const result = store.query({ service, statusCode, method: m, url, from, to, limit, offset, type, correlationId });
|
|
46
|
+
const result = await store.query({ service, statusCode, method: m, url, from, to, limit, offset, type, correlationId });
|
|
47
47
|
return { status: 200, headers: { 'content-type': 'application/json' }, body: result };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ── Single failure detail ──────────────────────────────────────────────
|
|
51
51
|
const detailMatch = path.match(/^\/observe\/api\/failures\/(\d+)$/);
|
|
52
52
|
if (method === 'GET' && detailMatch) {
|
|
53
|
-
const record = store.getById(detailMatch[1]);
|
|
53
|
+
const record = await store.getById(detailMatch[1]);
|
|
54
54
|
if (!record) {
|
|
55
55
|
return { status: 404, headers: { 'content-type': 'application/json' }, body: { error: 'Failure record not found' } };
|
|
56
56
|
}
|
|
@@ -59,12 +59,12 @@ function createRouter(store) {
|
|
|
59
59
|
|
|
60
60
|
// ── Summary stats ──────────────────────────────────────────────────────
|
|
61
61
|
if (method === 'GET' && path === '/observe/api/summary') {
|
|
62
|
-
return { status: 200, headers: { 'content-type': 'application/json' }, body: store.summary() };
|
|
62
|
+
return { status: 200, headers: { 'content-type': 'application/json' }, body: await store.summary() };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// ── Clear all ──────────────────────────────────────────────────────────
|
|
66
66
|
if (method === 'DELETE' && path === '/observe/api/failures') {
|
|
67
|
-
store.clear();
|
|
67
|
+
await store.clear();
|
|
68
68
|
return { status: 200, headers: { 'content-type': 'application/json' }, body: { cleared: true } };
|
|
69
69
|
}
|
|
70
70
|
|
package/lib/routes.js
CHANGED
|
@@ -22,9 +22,9 @@ function registerRoutes(fastify, store) {
|
|
|
22
22
|
const router = createRouter(store);
|
|
23
23
|
|
|
24
24
|
// Catch-all handler for /observe routes
|
|
25
|
-
const handler = (req, reply) => {
|
|
25
|
+
const handler = async (req, reply) => {
|
|
26
26
|
const url = new URL(req.url, 'http://localhost');
|
|
27
|
-
const result = router(req.method, url.pathname, req.query);
|
|
27
|
+
const result = await router(req.method, url.pathname, req.query);
|
|
28
28
|
if (!result) {
|
|
29
29
|
reply.code(404).send({ error: 'Not found' });
|
|
30
30
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-observe",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Framework-agnostic plugin that captures and displays API failure details — works with Fastify, Express, NestJS, Koa, or plain Node.js HTTP",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|