@piwitests/instrumentation-nitro 0.55.0 → 0.57.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/README.md +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +44 -25
- package/dist/manifest.d.ts +7 -1
- package/dist/manifest.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ During a Playwright test run, the reporter reads this header from every response
|
|
|
6
6
|
|
|
7
7
|
**Active in development and test only, by default.** Capture and probe verification run only outside production, using the same Development/Test allow-list as the ASP.NET package. Controlled by the `PIWI_TEST_LOGS_DISABLED` environment variable:
|
|
8
8
|
|
|
9
|
-
- unset — capture is on when `NODE_ENV` is
|
|
9
|
+
- unset — capture is on when `NODE_ENV` is `development` or `test`; off for any other value (`production`, `staging`, `prod`, …) and when `NODE_ENV` is not set at all, as when a built server is started with `node .output/server/index.mjs`
|
|
10
10
|
- `PIWI_TEST_LOGS_DISABLED=true` — capture is off everywhere
|
|
11
11
|
- `PIWI_TEST_LOGS_DISABLED=false` — capture is on even in production builds (useful for a production-mode test deployment)
|
|
12
12
|
|
|
@@ -42,7 +42,7 @@ export default defineNitroConfig({ modules: ['@piwitests/instrumentation-nitro/m
|
|
|
42
42
|
export default defineNuxtConfig({ nitro: { modules: ['@piwitests/instrumentation-nitro/module'] } })
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The manifest then lists every route Nitro scanned (`routes/`, `api/`) and every handler the config declares, middleware and a fallback catch-all (`/**`) left out, each with its method (`GET` for a route that answers any method), its pattern (`/api/orders/:id`) and its file relative to the git repository. Without the module, it lists only the routes the server has answered since it started, which the reporter, reading it before any test runs, rarely sees. Nuxt pages are rendered by one catch-all route, so they are not listed: name them in a `piwi.manifest.json`.
|
|
45
|
+
The manifest then lists every route Nitro scanned (`routes/`, `api/`) and every handler the config declares, middleware and a fallback catch-all (`/**`) left out, each with its method (`GET` for a route that answers any method), its pattern (`/api/orders/:id`) and its file relative to the git repository. Without the module, it lists only the routes the server has answered since it started (a route handler matched, so neither a 404 nor a public asset), which the reporter, reading it before any test runs, rarely sees. Nuxt pages are rendered by one catch-all route, so they are not listed: name them in a `piwi.manifest.json`.
|
|
46
46
|
|
|
47
47
|
## What gets captured
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ Each captured entry contains:
|
|
|
63
63
|
| `message` | Log message (truncated at 500 characters) |
|
|
64
64
|
| `stack` | Shrunk stack trace when an `Error` was logged (max 5 frames, internal frames dropped) |
|
|
65
65
|
|
|
66
|
-
Up to 50 entries per request are included. The header is always emitted (with an empty array when no entries were captured) so the Piwi reporter can confirm the plugin is active.
|
|
66
|
+
Up to 50 entries per request are included, the first ones logged; an entry logged once the response ended (a timer or a lazy client the request started) is dropped. A request the server makes to itself while rendering a page (`$fetch` or `useFetch` during server-side rendering) adds its entries and spans to the page's request. The header is always emitted (with an empty array when no entries were captured) so the Piwi reporter can confirm the plugin is active.
|
|
67
67
|
|
|
68
68
|
## How it works
|
|
69
69
|
|
package/dist/index.d.ts
CHANGED
|
@@ -39,7 +39,8 @@ export interface PiwiServerSpan {
|
|
|
39
39
|
/**
|
|
40
40
|
* Record a server-side span for the in-flight request. Shows up in the Piwi
|
|
41
41
|
* Dashboard test-case view (and AI diagnosis) under the request's root span.
|
|
42
|
-
* No-op outside a request scope
|
|
42
|
+
* No-op outside a request scope, once the per-request span cap is reached, or
|
|
43
|
+
* once the request's response has ended.
|
|
43
44
|
*/
|
|
44
45
|
export declare function recordServerSpan(span: PiwiServerSpan): void;
|
|
45
46
|
declare const piwiTestLogs: NitroAppPlugin;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { gzipSync } from 'node:zlib';
|
|
|
4
4
|
import { consola } from 'consola';
|
|
5
5
|
import { createError } from 'h3';
|
|
6
6
|
import { verifyProbeHeader, ProbeNonceCache } from './probe.js';
|
|
7
|
-
import { buildRouteManifest, recordObservedRoute,
|
|
7
|
+
import { buildRouteManifest, recordObservedRoute, matchedRoutePattern, } from './manifest.js';
|
|
8
8
|
import { declaredRoutes } from './route-table.js';
|
|
9
9
|
import { routeMatchesRequest, faultStatus, faultDelayMs, isThrowFault, isExtremeFault, isDataFault, isDependencyFault, dependencyCallMatches, mutateResponseBody, appliedFaultLabel, } from './faults.js';
|
|
10
10
|
export { verifyProbeHeader, signProbeMessage, probeSigningMessage, PROBE_TTL_MS, } from './probe.js';
|
|
@@ -57,6 +57,16 @@ function markProbeApplied(store) {
|
|
|
57
57
|
if (store.probe)
|
|
58
58
|
store.probeApplied = appliedFaultLabel(store.probe);
|
|
59
59
|
}
|
|
60
|
+
/** Record a log entry on a request still in flight, up to `MAX_ENTRIES`. */
|
|
61
|
+
function pushLog(store, entry) {
|
|
62
|
+
if (!store.ended && store.logs.length < MAX_ENTRIES)
|
|
63
|
+
store.logs.push(entry);
|
|
64
|
+
}
|
|
65
|
+
/** Record a span on a request still in flight, up to `MAX_SPANS`. */
|
|
66
|
+
function pushSpan(store, span) {
|
|
67
|
+
if (!store.ended && store.spans.length < MAX_SPANS)
|
|
68
|
+
store.spans.push(span);
|
|
69
|
+
}
|
|
60
70
|
/** Signed probe nonces already honored this process, so a header is single-use. */
|
|
61
71
|
const probeNonces = new ProbeNonceCache();
|
|
62
72
|
// Links consola calls and recorded spans to the request being handled. The store
|
|
@@ -66,24 +76,24 @@ const als = new AsyncLocalStorage();
|
|
|
66
76
|
/**
|
|
67
77
|
* Record a server-side span for the in-flight request. Shows up in the Piwi
|
|
68
78
|
* Dashboard test-case view (and AI diagnosis) under the request's root span.
|
|
69
|
-
* No-op outside a request scope
|
|
79
|
+
* No-op outside a request scope, once the per-request span cap is reached, or
|
|
80
|
+
* once the request's response has ended.
|
|
70
81
|
*/
|
|
71
82
|
export function recordServerSpan(span) {
|
|
72
83
|
const store = als.getStore();
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
store.spans.push(span);
|
|
84
|
+
if (store)
|
|
85
|
+
pushSpan(store, span);
|
|
76
86
|
}
|
|
77
87
|
// The consola reporter is process-global — register it only once.
|
|
78
88
|
let reporterAdded = false;
|
|
79
|
-
// Capture and probe verification run only
|
|
80
|
-
// package's default allow-list is Development and Test):
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// PIWI_TEST_LOGS_DISABLED is explicitly `false`, so a
|
|
84
|
-
//
|
|
89
|
+
// Capture and probe verification run only in development and test (the ASP.NET
|
|
90
|
+
// package's default allow-list is Development and Test): NODE_ENV must be
|
|
91
|
+
// `development` or `test`. Any other value, or none at all (`node
|
|
92
|
+
// .output/server/index.mjs` sets none), disables capture unless
|
|
93
|
+
// PIWI_TEST_LOGS_DISABLED is explicitly `false`, so a deployment that never set
|
|
94
|
+
// NODE_ENV exposes no server logs, handler files or route manifest.
|
|
85
95
|
const NODE_ENV = process.env.NODE_ENV;
|
|
86
|
-
const IS_DEV_OR_TEST =
|
|
96
|
+
const IS_DEV_OR_TEST = NODE_ENV === 'development' || NODE_ENV === 'test';
|
|
87
97
|
const TEST_LOGS_DISABLED = process.env.PIWI_TEST_LOGS_DISABLED === 'true' ||
|
|
88
98
|
(!IS_DEV_OR_TEST && process.env.PIWI_TEST_LOGS_DISABLED !== 'false');
|
|
89
99
|
/** The shared secret a probe run signs the `X-Piwi-Probe` header with. */
|
|
@@ -123,11 +133,11 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
123
133
|
if (logObj.level > 1)
|
|
124
134
|
return; // Warning (1) and Error/Fatal (0) only
|
|
125
135
|
const store = als.getStore();
|
|
126
|
-
if (!store)
|
|
136
|
+
if (!store || store.ended)
|
|
127
137
|
return;
|
|
128
138
|
const msg = logObj.args.map(String).join(' ');
|
|
129
139
|
const stack = logObj.args.map(extractStack).find(Boolean);
|
|
130
|
-
store
|
|
140
|
+
pushLog(store, {
|
|
131
141
|
timestamp: Date.now(),
|
|
132
142
|
level: logObj.level <= 0 ? 'Error' : 'Warning',
|
|
133
143
|
category: logObj.tag ?? '',
|
|
@@ -156,7 +166,13 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
156
166
|
res.end(JSON.stringify(buildRouteManifest(routes)));
|
|
157
167
|
return;
|
|
158
168
|
}
|
|
159
|
-
const
|
|
169
|
+
const enclosing = als.getStore();
|
|
170
|
+
const store = {
|
|
171
|
+
logs: [],
|
|
172
|
+
spans: [],
|
|
173
|
+
startMs: Date.now(),
|
|
174
|
+
...(enclosing && !enclosing.ended ? { parent: enclosing } : {}),
|
|
175
|
+
};
|
|
160
176
|
event.context._piwiLogs = store.logs;
|
|
161
177
|
event.context._piwiSpans = store.spans;
|
|
162
178
|
// The beforeResponse hook fires outside this handler's async scope (it is
|
|
@@ -204,7 +220,7 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
204
220
|
if (nitroErrors?.length) {
|
|
205
221
|
for (const { error } of nitroErrors) {
|
|
206
222
|
const msg = error instanceof Error ? (error.message || String(error)) : String(error);
|
|
207
|
-
store
|
|
223
|
+
pushLog(store, {
|
|
208
224
|
timestamp: Date.now(),
|
|
209
225
|
level: 'Error',
|
|
210
226
|
category: 'server',
|
|
@@ -213,8 +229,7 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
213
229
|
});
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
|
-
|
|
217
|
-
res.setHeader('X-Piwi-Logs', gzipSync(Buffer.from(JSON.stringify(logPayload))).toString('base64'));
|
|
232
|
+
res.setHeader('X-Piwi-Logs', gzipSync(Buffer.from(JSON.stringify(store.logs))).toString('base64'));
|
|
218
233
|
// Synthesize the root request span (server-side processing time, route,
|
|
219
234
|
// status), correlate any app-recorded child spans under it, and ship the
|
|
220
235
|
// whole tree. When the caller sent a W3C traceparent, reuse its trace id
|
|
@@ -225,13 +240,10 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
225
240
|
const statusCode = Number(res.statusCode) || 0;
|
|
226
241
|
const traceId = parseTraceparent(event.node.req.headers['traceparent']) ?? randomBytes(16).toString('hex');
|
|
227
242
|
const handlerFile = resolveHandlerFile(event);
|
|
228
|
-
// Record
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const matchedPattern = matchedRoute?.route ?? matchedRoute?.path;
|
|
233
|
-
const pattern = typeof matchedPattern === 'string' && matchedPattern ? matchedPattern : collapsePathPattern(path);
|
|
234
|
-
recordObservedRoute(observedRoutes, { method, pattern, handler: handlerFile });
|
|
243
|
+
// Record the route the router matched for the declared-surface manifest.
|
|
244
|
+
const pattern = matchedRoutePattern(event.context?.matchedRoute);
|
|
245
|
+
if (pattern)
|
|
246
|
+
recordObservedRoute(observedRoutes, { method, pattern, handler: handlerFile });
|
|
235
247
|
const rootSpan = {
|
|
236
248
|
id: randomBytes(8).toString('hex'),
|
|
237
249
|
name: `${method} ${path}`.trim(),
|
|
@@ -256,7 +268,14 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
256
268
|
s.parentId = rootSpan.id;
|
|
257
269
|
const spanPayload = [rootSpan, ...store.spans].slice(0, MAX_SPANS);
|
|
258
270
|
res.setHeader('X-Piwi-Trace', gzipSync(Buffer.from(JSON.stringify(spanPayload))).toString('base64'));
|
|
271
|
+
if (store.parent) {
|
|
272
|
+
for (const entry of store.logs)
|
|
273
|
+
pushLog(store.parent, entry);
|
|
274
|
+
for (const span of spanPayload)
|
|
275
|
+
pushSpan(store.parent, span);
|
|
276
|
+
}
|
|
259
277
|
}
|
|
278
|
+
store.ended = true;
|
|
260
279
|
return originalEnd(...args);
|
|
261
280
|
};
|
|
262
281
|
return als.run(store, async () => {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -20,9 +20,15 @@ export interface RouteManifest {
|
|
|
20
20
|
/**
|
|
21
21
|
* Collapse a concrete request path to a pattern: numeric and uuid segments become
|
|
22
22
|
* `:id` / `:uuid`, so `/api/orders/42` and `/api/orders/43` fold to one route.
|
|
23
|
-
* Used only when the matched route carries no pattern of its own.
|
|
24
23
|
*/
|
|
25
24
|
export declare function collapsePathPattern(path: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The pattern a request is recorded under: the one of the route the router
|
|
27
|
+
* matched it to (`path` in h3 1.x, `route` in later versions). Null when no
|
|
28
|
+
* route handler matched it: a 404, or a public asset served before the router,
|
|
29
|
+
* neither of which is a route of the app.
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchedRoutePattern(matchedRoute: unknown): string | null;
|
|
26
32
|
/**
|
|
27
33
|
* Record an observed route in the accumulator, keeping the first handler file
|
|
28
34
|
* seen for it. Bounded so a pathological app (or an unbounded set of patterns
|
package/dist/manifest.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Collapse a concrete request path to a pattern: numeric and uuid segments become
|
|
12
12
|
* `:id` / `:uuid`, so `/api/orders/42` and `/api/orders/43` fold to one route.
|
|
13
|
-
* Used only when the matched route carries no pattern of its own.
|
|
14
13
|
*/
|
|
15
14
|
export function collapsePathPattern(path) {
|
|
16
15
|
const clean = path.split('?')[0] ?? path;
|
|
@@ -27,6 +26,17 @@ export function collapsePathPattern(path) {
|
|
|
27
26
|
})
|
|
28
27
|
.join('/'));
|
|
29
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The pattern a request is recorded under: the one of the route the router
|
|
31
|
+
* matched it to (`path` in h3 1.x, `route` in later versions). Null when no
|
|
32
|
+
* route handler matched it: a 404, or a public asset served before the router,
|
|
33
|
+
* neither of which is a route of the app.
|
|
34
|
+
*/
|
|
35
|
+
export function matchedRoutePattern(matchedRoute) {
|
|
36
|
+
const matched = matchedRoute;
|
|
37
|
+
const pattern = matched?.route ?? matched?.path;
|
|
38
|
+
return typeof pattern === 'string' && pattern ? pattern : null;
|
|
39
|
+
}
|
|
30
40
|
/** The unique key for a route in the accumulator: method plus pattern. */
|
|
31
41
|
function routeKey(method, pattern) {
|
|
32
42
|
return `${method.toUpperCase()} ${pattern}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/instrumentation-nitro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Nitro/Nuxt server plugin for sending backend logs to Piwi Dashboard via the X-Piwi-Logs response header",
|
|
5
5
|
"homepage": "https://piwitests.dev",
|
|
6
6
|
"repository": {
|