api-observe 1.1.1 → 1.1.2

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.
@@ -30,6 +30,7 @@ const { parse: parseUrl } = require('url');
30
30
  const { FailureStore } = require('../failure-store');
31
31
  const { attachInterceptor, attachToAll, defaultSanitize } = require('../interceptor');
32
32
  const { createRouter } = require('../router');
33
+ const { requestContext } = require('../request-context');
33
34
 
34
35
  /**
35
36
  * Create an Express middleware that captures errors and serves the observe dashboard.
@@ -54,7 +55,7 @@ function expressMiddleware(opts = {}) {
54
55
 
55
56
  // Only handle /observe routes
56
57
  if (!pathname.startsWith('/observe')) {
57
- next();
58
+ requestContext.run({ captured: false, startedAt: Date.now(), inboundRequest: req }, next);
58
59
  return;
59
60
  }
60
61
 
@@ -502,24 +502,46 @@ function getDashboardHtml() {
502
502
  : ''
503
503
  ) +
504
504
 
505
- '<div class="detail-section"><h3>Request Headers</h3>' +
505
+ '<div class="detail-section"><h3>Inbound Request Headers</h3>' +
506
506
  '<pre class="json-block">' + escHtml(formatJson(f.request?.headers)) + '</pre></div>' +
507
507
 
508
508
  (f.request?.query
509
- ? '<div class="detail-section"><h3>Request Query Params</h3>' +
509
+ ? '<div class="detail-section"><h3>Inbound Query Params</h3>' +
510
510
  '<pre class="json-block">' + escHtml(formatJson(f.request?.query)) + '</pre></div>'
511
511
  : ''
512
512
  ) +
513
513
 
514
- '<div class="detail-section"><h3>Request Body</h3>' +
514
+ '<div class="detail-section"><h3>Inbound Request Body</h3>' +
515
515
  '<pre class="json-block">' + escHtml(formatJson(f.request?.body)) + '</pre></div>' +
516
516
 
517
- (f.response
518
- ? '<div class="detail-section"><h3>Response Headers</h3>' +
519
- '<pre class="json-block">' + escHtml(formatJson(f.response?.headers)) + '</pre></div>' +
520
- '<div class="detail-section"><h3>Response Body</h3>' +
521
- '<pre class="json-block">' + escHtml(formatJson(f.response?.body)) + '</pre></div>'
522
- : '<div class="detail-section"><h3>Response</h3><p>No response received (timeout / network error)</p></div>'
517
+ (f.upstreamCall
518
+ ? '<div class="detail-section"><h3>Upstream Call \u2014 ' + escHtml(f.upstreamCall.service) + '</h3>' +
519
+ '<dl class="detail-grid">' +
520
+ '<dt>Method</dt><dd><span class="method-badge method-' + escHtml(f.upstreamCall.method) + '">' + escHtml(f.upstreamCall.method) + '</span></dd>' +
521
+ '<dt>URL</dt><dd>' + escHtml(f.upstreamCall.url || '') + '</dd>' +
522
+ '<dt>Status Code</dt><dd>' + (f.upstreamCall.statusCode || 'N/A') + '</dd>' +
523
+ '<dt>Duration</dt><dd>' + (f.upstreamCall.durationMs != null ? f.upstreamCall.durationMs + 'ms' : 'N/A') + '</dd>' +
524
+ '<dt>Error</dt><dd>' + escHtml(f.upstreamCall.errorMessage || '') + '</dd>' +
525
+ (f.upstreamCall.errorCode ? '<dt>Error Code</dt><dd>' + escHtml(f.upstreamCall.errorCode) + '</dd>' : '') +
526
+ '</dl></div>' +
527
+ '<div class="detail-section"><h3>Upstream Request Headers</h3>' +
528
+ '<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.request?.headers)) + '</pre></div>' +
529
+ '<div class="detail-section"><h3>Upstream Request Body</h3>' +
530
+ '<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.request?.body)) + '</pre></div>' +
531
+ (f.upstreamCall.response
532
+ ? '<div class="detail-section"><h3>Upstream Response Headers</h3>' +
533
+ '<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.response?.headers)) + '</pre></div>' +
534
+ '<div class="detail-section"><h3>Upstream Response Body</h3>' +
535
+ '<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.response?.body)) + '</pre></div>'
536
+ : '<div class="detail-section"><h3>Upstream Response</h3><p>No response received (timeout / network error)</p></div>'
537
+ )
538
+ : (f.response
539
+ ? '<div class="detail-section"><h3>Response Headers</h3>' +
540
+ '<pre class="json-block">' + escHtml(formatJson(f.response?.headers)) + '</pre></div>' +
541
+ '<div class="detail-section"><h3>Response Body</h3>' +
542
+ '<pre class="json-block">' + escHtml(formatJson(f.response?.body)) + '</pre></div>'
543
+ : '<div class="detail-section"><h3>Response</h3><p>No response received (timeout / network error)</p></div>'
544
+ )
523
545
  );
524
546
 
525
547
  document.getElementById('modalOverlay').classList.add('active');
@@ -63,39 +63,75 @@ function attachInterceptor(client, store, opts = {}) {
63
63
  const response = error.response;
64
64
  const durationMs = Date.now() - (config.metadata?.startMs ?? Date.now());
65
65
 
66
- const record = {
67
- type: 'upstream',
66
+ const ctx = requestContext.getStore();
67
+ const inboundReq = ctx?.inboundRequest ?? null;
68
+
69
+ // ── Downstream call details ────────────────────────────────────────────
70
+ const upstreamCall = {
68
71
  service: client.serviceName,
69
72
  method: config.method?.toUpperCase() ?? 'UNKNOWN',
70
73
  url: buildFullUrl(config),
71
- correlationId: config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
72
74
  durationMs,
75
+ statusCode: response?.status ?? null,
73
76
  errorMessage: error.message,
74
77
  errorCode: error.code ?? null,
75
-
76
- // Request details
77
78
  request: {
78
79
  headers: sanitize(config.headers),
79
80
  params: config.params ?? null,
80
81
  body: sanitize(config.data),
81
82
  },
82
-
83
- // Response details (null if no response, e.g. timeout/network error)
84
- statusCode: response?.status ?? null,
85
83
  response: response
86
- ? {
87
- headers: response.headers ?? null,
88
- body: sanitize(response.data),
89
- }
84
+ ? { headers: response.headers ?? null, body: sanitize(response.data) }
90
85
  : null,
91
86
  };
92
87
 
88
+ let record;
89
+ if (inboundReq) {
90
+ // Top-level = the inbound request that triggered this failure.
91
+ // The downstream call details are nested under upstreamCall.
92
+ record = {
93
+ type: 'upstream',
94
+ service: inboundReq.routeOptions?.url ?? inboundReq.originalUrl ?? inboundReq.url ?? 'unknown',
95
+ method: inboundReq.method ?? 'UNKNOWN',
96
+ url: inboundReq.originalUrl ?? inboundReq.url ?? 'unknown',
97
+ correlationId: inboundReq.correlationId ?? inboundReq.headers?.['x-correlation-id'] ?? config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
98
+ durationMs,
99
+ errorMessage: error.message,
100
+ errorCode: error.code ?? null,
101
+ statusCode: response?.status ?? null,
102
+ request: {
103
+ headers: sanitize(inboundReq.headers),
104
+ params: inboundReq.params ?? null,
105
+ query: inboundReq.query ?? null,
106
+ body: sanitize(inboundReq.body),
107
+ },
108
+ response: null,
109
+ upstreamCall,
110
+ };
111
+ } else {
112
+ // No inbound context (standalone / direct interceptor usage) —
113
+ // keep the downstream call as the top-level record.
114
+ record = {
115
+ type: 'upstream',
116
+ service: client.serviceName,
117
+ method: upstreamCall.method,
118
+ url: upstreamCall.url,
119
+ correlationId: config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
120
+ durationMs,
121
+ errorMessage: error.message,
122
+ errorCode: error.code ?? null,
123
+ statusCode: response?.status ?? null,
124
+ request: upstreamCall.request,
125
+ response: upstreamCall.response,
126
+ upstreamCall: null,
127
+ };
128
+ }
129
+
93
130
  store.add(record);
94
131
 
95
132
  // Mark the in-flight Fastify request (if any) so the response-level
96
133
  // hook in plugin.js does not record a duplicate when the controller
97
134
  // catches this error and replies with its own 4xx/5xx.
98
- const ctx = requestContext.getStore();
99
135
  if (ctx) ctx.captured = true;
100
136
 
101
137
  // Re-throw the original error so BaseHttpClient's handler still works
package/lib/plugin.js CHANGED
@@ -88,7 +88,7 @@ async function observerPlugin(fastify, opts) {
88
88
  // interceptor records a capture deep in the call stack, it can reach back
89
89
  // and mark this request as already captured.
90
90
  fastify.addHook('onRequest', (request, reply, done) => {
91
- const ctx = { captured: false, startedAt: Date.now() };
91
+ const ctx = { captured: false, startedAt: Date.now(), inboundRequest: request };
92
92
  request[CTX_KEY] = ctx;
93
93
  requestContext.enterWith(ctx);
94
94
  done();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-observe",
3
- "version": "1.1.01",
3
+ "version": "1.1.2",
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": [