@piwitests/instrumentation-nitro 0.52.0 → 0.53.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 +15 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +20 -12
- package/dist/manifest.d.ts +5 -4
- package/dist/manifest.js +5 -4
- package/dist/module.d.ts +3 -0
- package/dist/module.js +33 -0
- package/dist/route-table.d.ts +30 -0
- package/dist/route-table.js +42 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -28,7 +28,21 @@ export { default } from '@piwitests/instrumentation-nitro'
|
|
|
28
28
|
|
|
29
29
|
That's all. Nitro auto-loads every file in that directory on startup (`server/plugins/` in a Nuxt app, `plugins/` under the Nitro `srcDir` in a standalone Nitro app).
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
[`examples/sample-app`](../../examples/sample-app) runs a standalone Nitro app with this package wired in (`backends/nitro/`), and a Playwright suite that checks what it sends: backend logs, the route manifest, server spans and server probes.
|
|
32
|
+
|
|
33
|
+
## Route manifest
|
|
34
|
+
|
|
35
|
+
Outside production, the plugin serves `GET /__piwi/manifest`: the routes the app declares. The Piwi reporter fetches it when a run starts and uploads it, so a declared route no test reaches becomes a **declared, never hit** gap in the Test Map. Nitro has no route table to read while it runs, so add the package's module, which reads it when the server is bundled (again on each rebuild in development):
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// Standalone Nitro: nitro.config.ts
|
|
39
|
+
export default defineNitroConfig({ modules: ['@piwitests/instrumentation-nitro/module'] })
|
|
40
|
+
|
|
41
|
+
// Nuxt: nuxt.config.ts
|
|
42
|
+
export default defineNuxtConfig({ nitro: { modules: ['@piwitests/instrumentation-nitro/module'] } })
|
|
43
|
+
```
|
|
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`.
|
|
32
46
|
|
|
33
47
|
## What gets captured
|
|
34
48
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NitroAppPlugin } from 'nitropack';
|
|
2
|
-
export { verifyProbeHeader, signProbeMessage, probeSigningMessage, PROBE_TTL_MS, type PiwiProbeSpec, type SignedProbe, } from './probe';
|
|
3
|
-
export { buildRouteManifest, recordObservedRoute, collapsePathPattern, type RouteManifest } from './manifest';
|
|
4
|
-
export * from './faults';
|
|
2
|
+
export { verifyProbeHeader, signProbeMessage, probeSigningMessage, PROBE_TTL_MS, type PiwiProbeSpec, type SignedProbe, } from './probe.js';
|
|
3
|
+
export { buildRouteManifest, recordObservedRoute, collapsePathPattern, type RouteManifest } from './manifest.js';
|
|
4
|
+
export * from './faults.js';
|
|
5
5
|
export interface PiwiTestLogEntry {
|
|
6
6
|
timestamp: number;
|
|
7
7
|
level: string;
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,13 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { gzipSync } from 'node:zlib';
|
|
4
4
|
import { consola } from 'consola';
|
|
5
5
|
import { createError } from 'h3';
|
|
6
|
-
import { verifyProbeHeader, ProbeNonceCache } from './probe';
|
|
7
|
-
import { buildRouteManifest, recordObservedRoute, collapsePathPattern, } from './manifest';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
export
|
|
6
|
+
import { verifyProbeHeader, ProbeNonceCache } from './probe.js';
|
|
7
|
+
import { buildRouteManifest, recordObservedRoute, collapsePathPattern, } from './manifest.js';
|
|
8
|
+
import { declaredRoutes } from './route-table.js';
|
|
9
|
+
import { routeMatchesRequest, faultStatus, faultDelayMs, isThrowFault, isExtremeFault, isDataFault, isDependencyFault, dependencyCallMatches, mutateResponseBody, appliedFaultLabel, } from './faults.js';
|
|
10
|
+
export { verifyProbeHeader, signProbeMessage, probeSigningMessage, PROBE_TTL_MS, } from './probe.js';
|
|
11
|
+
export { buildRouteManifest, recordObservedRoute, collapsePathPattern } from './manifest.js';
|
|
12
|
+
export * from './faults.js';
|
|
12
13
|
const MAX_ENTRIES = 50;
|
|
13
14
|
const MAX_MSG_LENGTH = 500;
|
|
14
15
|
const MAX_STACK_FRAMES = 5;
|
|
@@ -90,8 +91,9 @@ const PROBE_SECRET = process.env.PIWI_PROBE_SECRET || undefined;
|
|
|
90
91
|
/** The declared-surface route path the plugin serves outside production. */
|
|
91
92
|
const MANIFEST_PATH = '/__piwi/manifest';
|
|
92
93
|
/**
|
|
93
|
-
* Routes the server has matched this process, accumulated for `/__piwi/manifest
|
|
94
|
-
* Nitro exposes no runtime route table, so the
|
|
94
|
+
* Routes the server has matched this process, accumulated for `/__piwi/manifest`
|
|
95
|
+
* next to the declared ones: Nitro exposes no runtime route table, so without the
|
|
96
|
+
* module (`./module`, which reads it at build time) only these are listed.
|
|
95
97
|
*/
|
|
96
98
|
const observedRoutes = new Map();
|
|
97
99
|
/**
|
|
@@ -141,13 +143,17 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
141
143
|
const originalHandler = nitroApp.h3App.handler;
|
|
142
144
|
nitroApp.h3App.handler = ((event) => {
|
|
143
145
|
// Serve the declared-surface manifest outside production (the plugin already
|
|
144
|
-
// returned early when log capture is disabled)
|
|
146
|
+
// returned early when log capture is disabled): the routes the module declared,
|
|
147
|
+
// then the ones observed.
|
|
145
148
|
const requestPath = String(event.path ?? event.node.req.url ?? '').split('?')[0] ?? '';
|
|
146
149
|
if (requestPath === MANIFEST_PATH) {
|
|
150
|
+
const routes = new Map();
|
|
151
|
+
for (const route of [...declaredRoutes(), ...observedRoutes.values()])
|
|
152
|
+
recordObservedRoute(routes, route);
|
|
147
153
|
const res = event.node.res;
|
|
148
154
|
res.statusCode = 200;
|
|
149
155
|
res.setHeader('Content-Type', 'application/json');
|
|
150
|
-
res.end(JSON.stringify(buildRouteManifest(
|
|
156
|
+
res.end(JSON.stringify(buildRouteManifest(routes)));
|
|
151
157
|
return;
|
|
152
158
|
}
|
|
153
159
|
const store = { logs: [], spans: [], startMs: Date.now() };
|
|
@@ -220,8 +226,10 @@ const piwiTestLogs = (nitroApp) => {
|
|
|
220
226
|
const traceId = parseTraceparent(event.node.req.headers['traceparent']) ?? randomBytes(16).toString('hex');
|
|
221
227
|
const handlerFile = resolveHandlerFile(event);
|
|
222
228
|
// Record this route for the declared-surface manifest. Prefer the matched
|
|
223
|
-
// route's own pattern
|
|
224
|
-
|
|
229
|
+
// route's own pattern (`path` in h3 1.x, `route` in later versions); fall
|
|
230
|
+
// back to collapsing the concrete path.
|
|
231
|
+
const matchedRoute = event.context?.matchedRoute;
|
|
232
|
+
const matchedPattern = matchedRoute?.route ?? matchedRoute?.path;
|
|
225
233
|
const pattern = typeof matchedPattern === 'string' && matchedPattern ? matchedPattern : collapsePathPattern(path);
|
|
226
234
|
recordObservedRoute(observedRoutes, { method, pattern, handler: handlerFile });
|
|
227
235
|
const rootSpan = {
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The declared-surface route manifest the plugin serves at `/__piwi/manifest`
|
|
3
3
|
* outside production. Nitro has no stable public API to enumerate its route table
|
|
4
|
-
* at runtime
|
|
5
|
-
* matches
|
|
6
|
-
* file. The Piwi reporter fetches it once at
|
|
7
|
-
* declared route no test reaches becomes a
|
|
4
|
+
* at runtime: the module (`./module`) reads it at build time (`route-table.ts`),
|
|
5
|
+
* and the routes the server matches are added as they come, each request with its
|
|
6
|
+
* method, matched pattern and handler file. The Piwi reporter fetches it once at
|
|
7
|
+
* run start and uploads it, and a declared route no test reaches becomes a
|
|
8
|
+
* "declared, never hit" gap, which only the module's routes can give.
|
|
8
9
|
*/
|
|
9
10
|
/** One declared route: method, path pattern and the handler's source file. */
|
|
10
11
|
export interface ManifestRouteEntry {
|
package/dist/manifest.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The declared-surface route manifest the plugin serves at `/__piwi/manifest`
|
|
3
3
|
* outside production. Nitro has no stable public API to enumerate its route table
|
|
4
|
-
* at runtime
|
|
5
|
-
* matches
|
|
6
|
-
* file. The Piwi reporter fetches it once at
|
|
7
|
-
* declared route no test reaches becomes a
|
|
4
|
+
* at runtime: the module (`./module`) reads it at build time (`route-table.ts`),
|
|
5
|
+
* and the routes the server matches are added as they come, each request with its
|
|
6
|
+
* method, matched pattern and handler file. The Piwi reporter fetches it once at
|
|
7
|
+
* run start and uploads it, and a declared route no test reaches becomes a
|
|
8
|
+
* "declared, never hit" gap, which only the module's routes can give.
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* Collapse a concrete request path to a pattern: numeric and uuid segments become
|
package/dist/module.d.ts
ADDED
package/dist/module.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Nitro module that gives the route manifest Nitro's whole route table:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* export default defineNitroConfig({ modules: ['@piwitests/instrumentation-nitro/module'] });
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* In Nuxt, the same entry goes under `nitro.modules`. Without it, `/__piwi/manifest`
|
|
9
|
+
* lists only the routes the server has answered. Modules run before Nitro scans its
|
|
10
|
+
* route files, so the table is read when the server is bundled, again on each
|
|
11
|
+
* rebuild in development.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { ROUTE_TABLE_ID, declaredRoutesOf, routeTablePlugin } from './route-table.js';
|
|
16
|
+
/** The repository root above `dir` (the closest folder holding `.git`), else `dir` itself. */
|
|
17
|
+
function repositoryRoot(dir) {
|
|
18
|
+
for (let current = dir;; current = dirname(current)) {
|
|
19
|
+
if (existsSync(join(current, '.git')))
|
|
20
|
+
return current;
|
|
21
|
+
if (dirname(current) === current)
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const piwiRouteTable = {
|
|
26
|
+
name: 'piwi-route-table',
|
|
27
|
+
setup(nitro) {
|
|
28
|
+
const root = repositoryRoot(nitro.options.rootDir);
|
|
29
|
+
nitro.options.virtual[ROUTE_TABLE_ID] = () => routeTablePlugin(declaredRoutesOf([...nitro.scannedHandlers, ...nitro.options.handlers], root));
|
|
30
|
+
nitro.options.plugins.push(ROUTE_TABLE_ID);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export default piwiRouteTable;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The route table the Nitro module (`@piwitests/instrumentation-nitro/module`)
|
|
3
|
+
* reads at build time, from the routes Nitro scanned (`routes/`, `api/`) and the
|
|
4
|
+
* handlers its config declares. The module writes it into a small generated
|
|
5
|
+
* plugin, which hands it to the runtime plugin through a global, so
|
|
6
|
+
* `/__piwi/manifest` lists every declared route from the server's first request,
|
|
7
|
+
* reached or not.
|
|
8
|
+
*/
|
|
9
|
+
import type { ManifestRouteEntry } from './manifest.js';
|
|
10
|
+
/** The id of the generated plugin that carries the route table. */
|
|
11
|
+
export declare const ROUTE_TABLE_ID = "#piwi/route-table";
|
|
12
|
+
/** A Nitro handler, as `nitro.scannedHandlers` and `nitro.options.handlers` list it. */
|
|
13
|
+
export interface NitroHandlerLike {
|
|
14
|
+
route?: string;
|
|
15
|
+
method?: string;
|
|
16
|
+
handler?: string;
|
|
17
|
+
middleware?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The declared routes of Nitro's handlers: every handler with a route, middleware
|
|
21
|
+
* and a fallback catch-all left out, one entry per method and pattern. A handler
|
|
22
|
+
* without a method answers every method and is declared as `GET`. Its file is
|
|
23
|
+
* named relative to `root` (the repository root, else the app's root) with `/`
|
|
24
|
+
* separators.
|
|
25
|
+
*/
|
|
26
|
+
export declare function declaredRoutesOf(handlers: NitroHandlerLike[], root: string): ManifestRouteEntry[];
|
|
27
|
+
/** The source of the generated plugin: it sets the route table on the global the runtime plugin reads. */
|
|
28
|
+
export declare function routeTablePlugin(routes: ManifestRouteEntry[]): string;
|
|
29
|
+
/** The routes the generated plugin set; none when the app does not use the module. */
|
|
30
|
+
export declare function declaredRoutes(): ManifestRouteEntry[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The id of the generated plugin that carries the route table. */
|
|
2
|
+
export const ROUTE_TABLE_ID = '#piwi/route-table';
|
|
3
|
+
/** The global the generated plugin sets the route table on. */
|
|
4
|
+
const DECLARED_ROUTES_KEY = '__piwiDeclaredRoutes';
|
|
5
|
+
/** A route that is only a catch-all (`/**`, `/**:path`): an app's fallback, such as Nuxt's page renderer. */
|
|
6
|
+
const FALLBACK_ROUTE = /^\/\*\*(?::[^/]*)?$/;
|
|
7
|
+
/**
|
|
8
|
+
* The declared routes of Nitro's handlers: every handler with a route, middleware
|
|
9
|
+
* and a fallback catch-all left out, one entry per method and pattern. A handler
|
|
10
|
+
* without a method answers every method and is declared as `GET`. Its file is
|
|
11
|
+
* named relative to `root` (the repository root, else the app's root) with `/`
|
|
12
|
+
* separators.
|
|
13
|
+
*/
|
|
14
|
+
export function declaredRoutesOf(handlers, root) {
|
|
15
|
+
const routes = new Map();
|
|
16
|
+
for (const handler of handlers) {
|
|
17
|
+
if (handler.middleware || !handler.route || FALLBACK_ROUTE.test(handler.route))
|
|
18
|
+
continue;
|
|
19
|
+
const method = (handler.method || 'get').toUpperCase();
|
|
20
|
+
const key = `${method} ${handler.route}`;
|
|
21
|
+
if (routes.has(key))
|
|
22
|
+
continue;
|
|
23
|
+
const file = handler.handler ? relativeFile(root, handler.handler) : undefined;
|
|
24
|
+
routes.set(key, { method, pattern: handler.route, ...(file ? { handler: file } : {}) });
|
|
25
|
+
}
|
|
26
|
+
return [...routes.values()];
|
|
27
|
+
}
|
|
28
|
+
/** `file` relative to `root`, with `/` separators; undefined when it is not under `root`. */
|
|
29
|
+
function relativeFile(root, file) {
|
|
30
|
+
const normalizedRoot = root.replace(/\\/g, '/').replace(/\/$/, '');
|
|
31
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
32
|
+
return normalizedFile.startsWith(`${normalizedRoot}/`) ? normalizedFile.slice(normalizedRoot.length + 1) : undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The source of the generated plugin: it sets the route table on the global the runtime plugin reads. */
|
|
35
|
+
export function routeTablePlugin(routes) {
|
|
36
|
+
return `export default function piwiRouteTable() {\n globalThis.${DECLARED_ROUTES_KEY} = ${JSON.stringify(routes)};\n}\n`;
|
|
37
|
+
}
|
|
38
|
+
/** The routes the generated plugin set; none when the app does not use the module. */
|
|
39
|
+
export function declaredRoutes() {
|
|
40
|
+
const routes = globalThis[DECLARED_ROUTES_KEY];
|
|
41
|
+
return Array.isArray(routes) ? routes : [];
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/instrumentation-nitro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.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": {
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./module": {
|
|
22
|
+
"types": "./dist/module.d.ts",
|
|
23
|
+
"import": "./dist/module.js"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|