@nest-rn-lens/nest 0.1.1 → 0.2.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 +76 -3
- package/dist/cjs/capture.d.ts +25 -0
- package/dist/cjs/capture.js +105 -0
- package/dist/cjs/http.d.ts +4 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/interceptor.d.ts +1 -0
- package/dist/cjs/interceptor.js +24 -3
- package/dist/cjs/module.js +4 -0
- package/dist/cjs/reporter.d.ts +6 -0
- package/dist/cjs/reporter.js +15 -6
- package/dist/cjs/types.d.ts +41 -0
- package/dist/esm/capture.d.ts +25 -0
- package/dist/esm/capture.js +97 -0
- package/dist/esm/http.d.ts +4 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/interceptor.d.ts +1 -0
- package/dist/esm/interceptor.js +24 -3
- package/dist/esm/module.js +4 -0
- package/dist/esm/reporter.d.ts +6 -0
- package/dist/esm/reporter.js +14 -6
- package/dist/esm/types.d.ts +41 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
A NestJS interceptor that records every request your API handles: the app and
|
|
6
6
|
the exact file and line that sent it, the endpoint and the handler that answered,
|
|
7
|
-
the status,
|
|
7
|
+
the status, the time it took, and the request and response bodies (with secrets
|
|
8
|
+
redacted).
|
|
8
9
|
|
|
9
10
|
```
|
|
10
11
|
[NestRnLens] mobile (ios) apps/mobile/src/screens/order-details.tsx:23 → GET /orders/:id → OrdersController.findOne 200 4ms
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
It is the server half of **NestRN Lens**, a toolkit for Turborepo monorepos with
|
|
14
|
-
a NestJS API and a React Native (Expo) app. On its own it gives you readable
|
|
15
|
+
a NestJS API and a React Native (Expo) or Next.js app. On its own it gives you readable
|
|
15
16
|
request logs and a typed event for every call. With the [NestRN Lens VS Code
|
|
16
17
|
extension](https://marketplace.visualstudio.com/items?itemName=IsaiasDiaz.nest-rn-lens), those events become a live traffic panel where you can click any
|
|
17
18
|
request to open the screen that made it or the handler that answered it.
|
|
@@ -64,6 +65,22 @@ That's it. Every request handled by a controller is now logged:
|
|
|
64
65
|
Requests show `unknown` until the client says who it is. The next section
|
|
65
66
|
explains how.
|
|
66
67
|
|
|
68
|
+
## Step by step with React Native or Next.js
|
|
69
|
+
|
|
70
|
+
1. **API:** install the package and add `NestRnLensModule.forRoot({ app: 'api' })`
|
|
71
|
+
to your root module (above).
|
|
72
|
+
2. **API:** enable CORS in development (`app.enableCors()` in `main.ts`), since
|
|
73
|
+
web builds call the API from another origin. See [Browsers and CORS](#browsers-and-cors).
|
|
74
|
+
3. **App:** route your API calls through one helper that adds the
|
|
75
|
+
`x-nest-rn-lens-app` header (and, on the web, `x-nest-rn-lens-caller` with
|
|
76
|
+
`window.location.pathname`), only in development.
|
|
77
|
+
4. **Watch:** read the log lines, use `onEvent`, or open the
|
|
78
|
+
[NestRN Lens VS Code extension](https://marketplace.visualstudio.com/items?itemName=IsaiasDiaz.nest-rn-lens),
|
|
79
|
+
which starts everything and shows the traffic live.
|
|
80
|
+
|
|
81
|
+
Copy-paste helpers for React Native (Expo) and Next.js are in the extension's
|
|
82
|
+
guide: [Integrate your app, step by step](https://github.com/isa95Ar/nest-rn-lens-vscode#integrate-your-app-step-by-step).
|
|
83
|
+
|
|
67
84
|
## Telling the API who's calling
|
|
68
85
|
|
|
69
86
|
A request on its own doesn't say which app or which screen sent it. The client
|
|
@@ -72,7 +89,7 @@ adds that with three headers:
|
|
|
72
89
|
| Header | Example | Used for |
|
|
73
90
|
| ------------------------- | ----------------------------------------------- | ------------------------------------------------- |
|
|
74
91
|
| `x-nest-rn-lens-app` | `mobile` | Which app made the request |
|
|
75
|
-
| `x-nest-rn-lens-caller` | `/repo/apps/mobile/src/screens/orders.tsx:23` | The file and line that made it
|
|
92
|
+
| `x-nest-rn-lens-caller` | `/repo/apps/mobile/src/screens/orders.tsx:23` | The file and line that made it, or the page path for web apps (`/orders/42`) |
|
|
76
93
|
| `x-nest-rn-lens-trace-id` | `5f0c…` | Linking every hop of one request chain |
|
|
77
94
|
|
|
78
95
|
You don't have to write these by hand. The upcoming React Native client,
|
|
@@ -105,6 +122,9 @@ NestRnLensModule.forRoot({
|
|
|
105
122
|
enabled: true,
|
|
106
123
|
log: true,
|
|
107
124
|
onEvent: (event) => {},
|
|
125
|
+
captureBodies: true,
|
|
126
|
+
maxBodyBytes: 16 * 1024,
|
|
127
|
+
redactKeys: ['pin'],
|
|
108
128
|
});
|
|
109
129
|
```
|
|
110
130
|
|
|
@@ -114,6 +134,9 @@ NestRnLensModule.forRoot({
|
|
|
114
134
|
| `enabled` | `boolean` | `true` unless `NODE_ENV=production` | Turns the interceptor on or off. When off, requests pass through untouched. |
|
|
115
135
|
| `log` | `boolean` | `true` | Logs a readable line per request, plus the full event as JSON at debug level. |
|
|
116
136
|
| `onEvent` | `(event: NestRnLensEvent) => void` | none | Called with every event. Forward traffic to your own tooling, tests or metrics. |
|
|
137
|
+
| `captureBodies` | `boolean` | `true` | Adds the request (headers, query, route params, body) and the response body to each event. |
|
|
138
|
+
| `maxBodyBytes` | `number` | `16384` (16 KB) | Bodies larger than this are cut to a preview of that many characters. |
|
|
139
|
+
| `redactKeys` | `string[]` | `[]` | Extra field or header names to hide, added to the built-in list (see below). |
|
|
117
140
|
|
|
118
141
|
### The event
|
|
119
142
|
|
|
@@ -138,13 +161,61 @@ interface NestRnLensEvent {
|
|
|
138
161
|
};
|
|
139
162
|
status: number; // 200, 201, 404, 500…
|
|
140
163
|
error?: string; // exception message, when the handler threw
|
|
164
|
+
request?: {
|
|
165
|
+
headers: Record<string, string>; // sensitive ones redacted
|
|
166
|
+
query?: unknown; // { type: "water" }
|
|
167
|
+
params?: unknown; // { id: "42" } for /orders/:id
|
|
168
|
+
body?: CapturedBody;
|
|
169
|
+
};
|
|
170
|
+
response?: {
|
|
171
|
+
body?: CapturedBody; // the handler's return value, or the error body Nest sends
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface CapturedBody {
|
|
176
|
+
size: number; // bytes, once serialized
|
|
177
|
+
value?: unknown; // the body, redacted (absent when truncated or summarized)
|
|
178
|
+
truncated?: boolean; // larger than maxBodyBytes...
|
|
179
|
+
preview?: string; // ...so this holds its start
|
|
180
|
+
summary?: string; // "[binary 12.0 KB]", "[stream]" for non-JSON bodies
|
|
141
181
|
}
|
|
142
182
|
```
|
|
143
183
|
|
|
184
|
+
`request` and `response` are present when `captureBodies` is on (the default).
|
|
185
|
+
|
|
144
186
|
`route` is the route pattern, not the URL, so all calls to one endpoint group
|
|
145
187
|
together. `status` follows Nest's rules: `@HttpCode()` when set, `201` for
|
|
146
188
|
`POST`, the exception's status when a handler throws, and `500` for other errors.
|
|
147
189
|
|
|
190
|
+
## Request and response bodies
|
|
191
|
+
|
|
192
|
+
Each event carries what the client sent and what the API answered, so you can
|
|
193
|
+
see the data behind every request, not just its status:
|
|
194
|
+
|
|
195
|
+
- **Request:** headers, query parameters, route parameters and the parsed body.
|
|
196
|
+
- **Response:** the value the handler returned. When a handler throws, it's the
|
|
197
|
+
error body Nest sends, for example
|
|
198
|
+
`{ "statusCode": 404, "message": "Order #42 not found", "error": "Not Found" }`.
|
|
199
|
+
|
|
200
|
+
**Secrets are redacted.** Any field or header whose name contains one of these
|
|
201
|
+
words is replaced with `"[redacted]"`, at any depth, in bodies, query, params
|
|
202
|
+
and headers:
|
|
203
|
+
|
|
204
|
+
`password`, `passwd`, `secret`, `token`, `authorization`, `cookie`, `apiKey`,
|
|
205
|
+
`privateKey`, `creditCard`, `cardNumber`, `cvv`, `ssn`
|
|
206
|
+
|
|
207
|
+
Matching ignores case and separators, so `api_key`, `API-Key` and `apiKey` are
|
|
208
|
+
all covered, and `token` also hides `accessToken` and `refreshToken`. Add your
|
|
209
|
+
own with `redactKeys`.
|
|
210
|
+
|
|
211
|
+
**Large and binary bodies are summarized.** A body bigger than `maxBodyBytes`
|
|
212
|
+
(16 KB by default) keeps only a text preview of its start. Files and streams
|
|
213
|
+
(`StreamableFile`, buffers) show as `[binary 12.0 KB]` or `[stream]`, and are
|
|
214
|
+
never read.
|
|
215
|
+
|
|
216
|
+
Turn it all off with `captureBodies: false`, for example if your API handles
|
|
217
|
+
data that shouldn't appear in development logs at all.
|
|
218
|
+
|
|
148
219
|
## Safe by design
|
|
149
220
|
|
|
150
221
|
NestRN Lens is a development tool. It is built so that it can't hurt the API it
|
|
@@ -155,6 +226,8 @@ watches:
|
|
|
155
226
|
- **Never breaks a request.** If your `onEvent` callback or the logger throws,
|
|
156
227
|
the error is swallowed and the response goes out as usual.
|
|
157
228
|
- **Doesn't change responses.** The only thing it adds is the trace id header.
|
|
229
|
+
- **Redacts secrets.** Passwords, tokens, cookies and similar fields never
|
|
230
|
+
appear in captured bodies or headers (see above).
|
|
158
231
|
- **HTTP only.** Microservice, WebSocket and GraphQL contexts pass straight
|
|
159
232
|
through.
|
|
160
233
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CapturedBody } from './types.js';
|
|
2
|
+
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* Field and header names whose values are never captured, matched ignoring case
|
|
5
|
+
* and separators ("api_key", "apiKey" and "API-Key" are the same).
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_REDACT_KEYS: string[];
|
|
8
|
+
export declare const REDACTED = "[redacted]";
|
|
9
|
+
export interface CaptureOptions {
|
|
10
|
+
maxBodyBytes: number;
|
|
11
|
+
redactKeys: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function isSensitive(key: string, redactKeys: string[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Turns a request or response body into something safe to log: sensitive
|
|
16
|
+
* fields redacted, binary data and streams summarized, and anything larger
|
|
17
|
+
* than `maxBodyBytes` cut to a text preview.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureBody(value: unknown, { maxBodyBytes, redactKeys }: CaptureOptions): CapturedBody | undefined;
|
|
20
|
+
/** Request headers with sensitive values replaced. */
|
|
21
|
+
export declare function captureHeaders(headers: Record<string, string | string[] | undefined>, redactKeys: string[]): Record<string, string>;
|
|
22
|
+
/** The body Nest sends for an exception: HttpException's response, or its generic 500. */
|
|
23
|
+
export declare function errorResponseBody(error: unknown): unknown;
|
|
24
|
+
/** Leaves out empty query and params objects: there's nothing to show. */
|
|
25
|
+
export declare function nonEmpty(value: unknown): unknown;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REDACTED = exports.DEFAULT_REDACT_KEYS = exports.DEFAULT_MAX_BODY_BYTES = void 0;
|
|
4
|
+
exports.isSensitive = isSensitive;
|
|
5
|
+
exports.captureBody = captureBody;
|
|
6
|
+
exports.captureHeaders = captureHeaders;
|
|
7
|
+
exports.errorResponseBody = errorResponseBody;
|
|
8
|
+
exports.nonEmpty = nonEmpty;
|
|
9
|
+
const common_1 = require("@nestjs/common");
|
|
10
|
+
exports.DEFAULT_MAX_BODY_BYTES = 16 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Field and header names whose values are never captured, matched ignoring case
|
|
13
|
+
* and separators ("api_key", "apiKey" and "API-Key" are the same).
|
|
14
|
+
*/
|
|
15
|
+
exports.DEFAULT_REDACT_KEYS = [
|
|
16
|
+
'password',
|
|
17
|
+
'passwd',
|
|
18
|
+
'secret',
|
|
19
|
+
'token',
|
|
20
|
+
'authorization',
|
|
21
|
+
'cookie',
|
|
22
|
+
'apikey',
|
|
23
|
+
'privatekey',
|
|
24
|
+
'creditcard',
|
|
25
|
+
'cardnumber',
|
|
26
|
+
'cvv',
|
|
27
|
+
'ssn',
|
|
28
|
+
];
|
|
29
|
+
exports.REDACTED = '[redacted]';
|
|
30
|
+
const normalize = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
31
|
+
function isSensitive(key, redactKeys) {
|
|
32
|
+
const name = normalize(key);
|
|
33
|
+
return name.length > 0 && redactKeys.some((pattern) => name.includes(normalize(pattern)));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Turns a request or response body into something safe to log: sensitive
|
|
37
|
+
* fields redacted, binary data and streams summarized, and anything larger
|
|
38
|
+
* than `maxBodyBytes` cut to a text preview.
|
|
39
|
+
*/
|
|
40
|
+
function captureBody(value, { maxBodyBytes, redactKeys }) {
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (isBinary(value)) {
|
|
45
|
+
return { size: value.byteLength, summary: `[binary ${formatBytes(value.byteLength)}]` };
|
|
46
|
+
}
|
|
47
|
+
if (isStream(value)) {
|
|
48
|
+
return { size: 0, summary: '[stream]' };
|
|
49
|
+
}
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.stringify(value, (key, field) => (key && isSensitive(key, redactKeys) ? exports.REDACTED : field));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { size: 0, summary: '[could not be serialized]' };
|
|
56
|
+
}
|
|
57
|
+
if (json === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const size = Buffer.byteLength(json);
|
|
61
|
+
if (size > maxBodyBytes) {
|
|
62
|
+
return { size, truncated: true, preview: json.slice(0, maxBodyBytes) };
|
|
63
|
+
}
|
|
64
|
+
return { size, value: JSON.parse(json) };
|
|
65
|
+
}
|
|
66
|
+
/** Request headers with sensitive values replaced. */
|
|
67
|
+
function captureHeaders(headers, redactKeys) {
|
|
68
|
+
const captured = {};
|
|
69
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
captured[name] = isSensitive(name, redactKeys) ? exports.REDACTED : Array.isArray(value) ? value.join(', ') : value;
|
|
74
|
+
}
|
|
75
|
+
return captured;
|
|
76
|
+
}
|
|
77
|
+
/** The body Nest sends for an exception: HttpException's response, or its generic 500. */
|
|
78
|
+
function errorResponseBody(error) {
|
|
79
|
+
if (error instanceof common_1.HttpException) {
|
|
80
|
+
const response = error.getResponse();
|
|
81
|
+
return typeof response === 'string' ? { statusCode: error.getStatus(), message: response } : response;
|
|
82
|
+
}
|
|
83
|
+
return { statusCode: 500, message: 'Internal server error' };
|
|
84
|
+
}
|
|
85
|
+
/** Leaves out empty query and params objects: there's nothing to show. */
|
|
86
|
+
function nonEmpty(value) {
|
|
87
|
+
if (value && typeof value === 'object' && Object.keys(value).length === 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function isBinary(value) {
|
|
93
|
+
return value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
94
|
+
}
|
|
95
|
+
// Readable streams and Nest's StreamableFile.
|
|
96
|
+
function isStream(value) {
|
|
97
|
+
if (!value || typeof value !== 'object') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const candidate = value;
|
|
101
|
+
return typeof candidate.pipe === 'function' || typeof candidate.getStream === 'function';
|
|
102
|
+
}
|
|
103
|
+
function formatBytes(bytes) {
|
|
104
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
105
|
+
}
|
package/dist/cjs/http.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface HttpRequest {
|
|
|
15
15
|
};
|
|
16
16
|
/** Fastify 3. */
|
|
17
17
|
routerPath?: string;
|
|
18
|
+
/** Parsed by Nest's body parser (Express) or by Fastify. */
|
|
19
|
+
body?: unknown;
|
|
20
|
+
query?: unknown;
|
|
21
|
+
params?: unknown;
|
|
18
22
|
}
|
|
19
23
|
export interface HttpResponse {
|
|
20
24
|
statusCode: number;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { NestRnLensModule } from './module.js';
|
|
2
2
|
export { NestRnLensInterceptor } from './interceptor.js';
|
|
3
3
|
export { NEST_RN_LENS_HEADERS } from './types.js';
|
|
4
|
-
export type { NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
|
|
4
|
+
export type { CapturedBody, NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
|
|
@@ -11,6 +11,7 @@ export declare class NestRnLensInterceptor implements NestInterceptor {
|
|
|
11
11
|
private readonly reporter;
|
|
12
12
|
constructor(options: NestRnLensOptions, reporter: NestRnLensReporter);
|
|
13
13
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
14
|
+
private captureOptions;
|
|
14
15
|
/**
|
|
15
16
|
* The status Nest is about to send. Depending on the platform it may not be
|
|
16
17
|
* written to the response yet, so follow Nest's own rules: @HttpCode(),
|
package/dist/cjs/interceptor.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.NestRnLensInterceptor = void 0;
|
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const node_crypto_1 = require("node:crypto");
|
|
18
18
|
const rxjs_1 = require("rxjs");
|
|
19
|
+
const capture_js_1 = require("./capture.js");
|
|
19
20
|
const constants_js_1 = require("./constants.js");
|
|
20
21
|
const http_js_1 = require("./http.js");
|
|
21
22
|
const reporter_js_1 = require("./reporter.js");
|
|
@@ -46,7 +47,15 @@ let NestRnLensInterceptor = class NestRnLensInterceptor {
|
|
|
46
47
|
const traceId = (0, http_js_1.readHeader)(req, types_js_1.NEST_RN_LENS_HEADERS.traceId) ?? (0, node_crypto_1.randomUUID)();
|
|
47
48
|
// Lets the client, and the next service in the chain, reuse the trace id.
|
|
48
49
|
(0, http_js_1.writeHeader)(res, types_js_1.NEST_RN_LENS_HEADERS.traceId, traceId);
|
|
49
|
-
|
|
50
|
+
// Captured before the handler runs, in case it mutates the request.
|
|
51
|
+
const capture = this.captureOptions();
|
|
52
|
+
const request = capture && {
|
|
53
|
+
headers: (0, capture_js_1.captureHeaders)(req.headers, capture.redactKeys),
|
|
54
|
+
query: captureValue((0, capture_js_1.nonEmpty)(req.query), capture),
|
|
55
|
+
params: captureValue((0, capture_js_1.nonEmpty)(req.params), capture),
|
|
56
|
+
body: (0, capture_js_1.captureBody)(req.body, capture),
|
|
57
|
+
};
|
|
58
|
+
const finish = (status, error, responseBody) => {
|
|
50
59
|
const event = {
|
|
51
60
|
id: (0, node_crypto_1.randomUUID)(),
|
|
52
61
|
traceId,
|
|
@@ -67,14 +76,22 @@ let NestRnLensInterceptor = class NestRnLensInterceptor {
|
|
|
67
76
|
},
|
|
68
77
|
status,
|
|
69
78
|
error,
|
|
79
|
+
...(capture && { request, response: { body: (0, capture_js_1.captureBody)(responseBody, capture) } }),
|
|
70
80
|
};
|
|
71
81
|
this.reporter.report(event);
|
|
72
82
|
};
|
|
73
83
|
return next.handle().pipe((0, rxjs_1.tap)({
|
|
74
|
-
next: () => finish(this.successStatus(context, req, res)),
|
|
75
|
-
error: (err) => finish(err instanceof common_1.HttpException ? err.getStatus() : 500, err instanceof Error ? err.message : String(err)),
|
|
84
|
+
next: (body) => finish(this.successStatus(context, req, res), undefined, body),
|
|
85
|
+
error: (err) => finish(err instanceof common_1.HttpException ? err.getStatus() : 500, err instanceof Error ? err.message : String(err), (0, capture_js_1.errorResponseBody)(err)),
|
|
76
86
|
}));
|
|
77
87
|
}
|
|
88
|
+
captureOptions() {
|
|
89
|
+
const { captureBodies, maxBodyBytes, redactKeys } = this.options;
|
|
90
|
+
if (captureBodies === false) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return { maxBodyBytes: maxBodyBytes ?? capture_js_1.DEFAULT_MAX_BODY_BYTES, redactKeys: redactKeys ?? capture_js_1.DEFAULT_REDACT_KEYS };
|
|
94
|
+
}
|
|
78
95
|
/**
|
|
79
96
|
* The status Nest is about to send. Depending on the platform it may not be
|
|
80
97
|
* written to the response yet, so follow Nest's own rules: @HttpCode(),
|
|
@@ -98,3 +115,7 @@ exports.NestRnLensInterceptor = NestRnLensInterceptor = __decorate([
|
|
|
98
115
|
__param(0, (0, common_1.Inject)(constants_js_1.NEST_RN_LENS_OPTIONS)),
|
|
99
116
|
__metadata("design:paramtypes", [Object, reporter_js_1.NestRnLensReporter])
|
|
100
117
|
], NestRnLensInterceptor);
|
|
118
|
+
/** Small values (query, params) keep their structure; their redacted value is all we need. */
|
|
119
|
+
function captureValue(value, capture) {
|
|
120
|
+
return (0, capture_js_1.captureBody)(value, capture)?.value;
|
|
121
|
+
}
|
package/dist/cjs/module.js
CHANGED
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
exports.NestRnLensModule = void 0;
|
|
11
11
|
const common_1 = require("@nestjs/common");
|
|
12
12
|
const core_1 = require("@nestjs/core");
|
|
13
|
+
const capture_js_1 = require("./capture.js");
|
|
13
14
|
const constants_js_1 = require("./constants.js");
|
|
14
15
|
const interceptor_js_1 = require("./interceptor.js");
|
|
15
16
|
const reporter_js_1 = require("./reporter.js");
|
|
@@ -27,6 +28,9 @@ let NestRnLensModule = NestRnLensModule_1 = class NestRnLensModule {
|
|
|
27
28
|
...options,
|
|
28
29
|
enabled: options.enabled ?? process.env.NODE_ENV !== 'production',
|
|
29
30
|
log: options.log ?? true,
|
|
31
|
+
captureBodies: options.captureBodies ?? true,
|
|
32
|
+
maxBodyBytes: options.maxBodyBytes ?? capture_js_1.DEFAULT_MAX_BODY_BYTES,
|
|
33
|
+
redactKeys: [...capture_js_1.DEFAULT_REDACT_KEYS, ...(options.redactKeys ?? [])],
|
|
30
34
|
};
|
|
31
35
|
return {
|
|
32
36
|
module: NestRnLensModule_1,
|
package/dist/cjs/reporter.d.ts
CHANGED
|
@@ -10,3 +10,9 @@ export declare class NestRnLensReporter {
|
|
|
10
10
|
private describe;
|
|
11
11
|
private shortPath;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Callers that are source locations are shown relative to the monorepo root
|
|
15
|
+
* (clients send absolute paths so editors can open them). Anything else, such as
|
|
16
|
+
* a web page path like "/orders/42", is shown as sent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatCaller(caller: string, repoRoot: string): string;
|
package/dist/cjs/reporter.js
CHANGED
|
@@ -13,6 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.NestRnLensReporter = void 0;
|
|
16
|
+
exports.formatCaller = formatCaller;
|
|
16
17
|
const common_1 = require("@nestjs/common");
|
|
17
18
|
const node_fs_1 = require("node:fs");
|
|
18
19
|
const node_path_1 = require("node:path");
|
|
@@ -48,14 +49,12 @@ let NestRnLensReporter = class NestRnLensReporter {
|
|
|
48
49
|
const from = [`${source.app} (${source.platform})`, this.shortPath(source.caller)].filter(Boolean).join(' ');
|
|
49
50
|
return `${from} → ${target.method} ${target.route} → ${target.controller}.${target.handler} ${status} ${durationMs}ms`;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!file?.startsWith('/')) {
|
|
55
|
-
return file;
|
|
52
|
+
shortPath(caller) {
|
|
53
|
+
if (!caller || !SOURCE_LOCATION.test(caller)) {
|
|
54
|
+
return caller;
|
|
56
55
|
}
|
|
57
56
|
this.repoRoot ??= findRepoRoot(process.cwd());
|
|
58
|
-
return (
|
|
57
|
+
return formatCaller(caller, this.repoRoot);
|
|
59
58
|
}
|
|
60
59
|
};
|
|
61
60
|
exports.NestRnLensReporter = NestRnLensReporter;
|
|
@@ -64,6 +63,16 @@ exports.NestRnLensReporter = NestRnLensReporter = __decorate([
|
|
|
64
63
|
__param(0, (0, common_1.Inject)(constants_js_1.NEST_RN_LENS_OPTIONS)),
|
|
65
64
|
__metadata("design:paramtypes", [Object])
|
|
66
65
|
], NestRnLensReporter);
|
|
66
|
+
// "/repo/apps/mobile/src/screens/Orders.tsx:23": an absolute file path plus a line.
|
|
67
|
+
const SOURCE_LOCATION = /^\/.+:\d+$/;
|
|
68
|
+
/**
|
|
69
|
+
* Callers that are source locations are shown relative to the monorepo root
|
|
70
|
+
* (clients send absolute paths so editors can open them). Anything else, such as
|
|
71
|
+
* a web page path like "/orders/42", is shown as sent.
|
|
72
|
+
*/
|
|
73
|
+
function formatCaller(caller, repoRoot) {
|
|
74
|
+
return SOURCE_LOCATION.test(caller) ? (0, node_path_1.relative)(repoRoot, caller) : caller;
|
|
75
|
+
}
|
|
67
76
|
const ROOT_MARKERS = ['turbo.json', 'pnpm-workspace.yaml', '.git'];
|
|
68
77
|
function findRepoRoot(from) {
|
|
69
78
|
let dir = from;
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -11,6 +11,18 @@ export declare const NEST_RN_LENS_HEADERS: {
|
|
|
11
11
|
readonly traceId: "x-nest-rn-lens-trace-id";
|
|
12
12
|
};
|
|
13
13
|
export type Platform = 'ios' | 'android' | 'web' | 'unknown';
|
|
14
|
+
/** A request or response body, made safe to log. */
|
|
15
|
+
export interface CapturedBody {
|
|
16
|
+
/** Size of the serialized body, in bytes. */
|
|
17
|
+
size: number;
|
|
18
|
+
/** The body, with sensitive fields redacted. Absent when truncated or summarized. */
|
|
19
|
+
value?: unknown;
|
|
20
|
+
/** Set when the body was larger than `maxBodyBytes`; `preview` holds its start. */
|
|
21
|
+
truncated?: boolean;
|
|
22
|
+
preview?: string;
|
|
23
|
+
/** For bodies that aren't JSON: "[binary 12.0 KB]", "[stream]". */
|
|
24
|
+
summary?: string;
|
|
25
|
+
}
|
|
14
26
|
/** One request, as seen by the API. */
|
|
15
27
|
export interface NestRnLensEvent {
|
|
16
28
|
id: string;
|
|
@@ -40,6 +52,20 @@ export interface NestRnLensEvent {
|
|
|
40
52
|
status: number;
|
|
41
53
|
/** Message of the exception, when the handler threw. */
|
|
42
54
|
error?: string;
|
|
55
|
+
/** What the client sent. Only present when `captureBodies` is on (the default). */
|
|
56
|
+
request?: {
|
|
57
|
+
/** Request headers; sensitive ones redacted. */
|
|
58
|
+
headers: Record<string, string>;
|
|
59
|
+
query?: unknown;
|
|
60
|
+
/** Route parameters, e.g. { id: "42" } for /orders/:id. */
|
|
61
|
+
params?: unknown;
|
|
62
|
+
body?: CapturedBody;
|
|
63
|
+
};
|
|
64
|
+
/** What the API answered. Only present when `captureBodies` is on (the default). */
|
|
65
|
+
response?: {
|
|
66
|
+
/** The handler's return value, or the error body Nest sends for an exception. */
|
|
67
|
+
body?: CapturedBody;
|
|
68
|
+
};
|
|
43
69
|
}
|
|
44
70
|
export interface NestRnLensOptions {
|
|
45
71
|
/** Name of this API in the traffic graph, e.g. "api". */
|
|
@@ -59,4 +85,19 @@ export interface NestRnLensOptions {
|
|
|
59
85
|
* debug level that the NestRN Lens VS Code extension reads). Defaults to `true`.
|
|
60
86
|
*/
|
|
61
87
|
log?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Include request and response bodies, query, route params and request
|
|
90
|
+
* headers in each event. Defaults to `true`. Sensitive fields are redacted
|
|
91
|
+
* (see `redactKeys`).
|
|
92
|
+
*/
|
|
93
|
+
captureBodies?: boolean;
|
|
94
|
+
/** Bodies larger than this are cut to a preview. Defaults to 16 KB. */
|
|
95
|
+
maxBodyBytes?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Extra field or header names to redact, added to the defaults (password,
|
|
98
|
+
* secret, token, authorization, cookie, apiKey, privateKey, creditCard,
|
|
99
|
+
* cardNumber, cvv, ssn). Matched ignoring case and separators, anywhere in
|
|
100
|
+
* the name: "token" also covers "accessToken".
|
|
101
|
+
*/
|
|
102
|
+
redactKeys?: string[];
|
|
62
103
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CapturedBody } from './types.js';
|
|
2
|
+
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* Field and header names whose values are never captured, matched ignoring case
|
|
5
|
+
* and separators ("api_key", "apiKey" and "API-Key" are the same).
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_REDACT_KEYS: string[];
|
|
8
|
+
export declare const REDACTED = "[redacted]";
|
|
9
|
+
export interface CaptureOptions {
|
|
10
|
+
maxBodyBytes: number;
|
|
11
|
+
redactKeys: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function isSensitive(key: string, redactKeys: string[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Turns a request or response body into something safe to log: sensitive
|
|
16
|
+
* fields redacted, binary data and streams summarized, and anything larger
|
|
17
|
+
* than `maxBodyBytes` cut to a text preview.
|
|
18
|
+
*/
|
|
19
|
+
export declare function captureBody(value: unknown, { maxBodyBytes, redactKeys }: CaptureOptions): CapturedBody | undefined;
|
|
20
|
+
/** Request headers with sensitive values replaced. */
|
|
21
|
+
export declare function captureHeaders(headers: Record<string, string | string[] | undefined>, redactKeys: string[]): Record<string, string>;
|
|
22
|
+
/** The body Nest sends for an exception: HttpException's response, or its generic 500. */
|
|
23
|
+
export declare function errorResponseBody(error: unknown): unknown;
|
|
24
|
+
/** Leaves out empty query and params objects: there's nothing to show. */
|
|
25
|
+
export declare function nonEmpty(value: unknown): unknown;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HttpException } from '@nestjs/common';
|
|
2
|
+
export const DEFAULT_MAX_BODY_BYTES = 16 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Field and header names whose values are never captured, matched ignoring case
|
|
5
|
+
* and separators ("api_key", "apiKey" and "API-Key" are the same).
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_REDACT_KEYS = [
|
|
8
|
+
'password',
|
|
9
|
+
'passwd',
|
|
10
|
+
'secret',
|
|
11
|
+
'token',
|
|
12
|
+
'authorization',
|
|
13
|
+
'cookie',
|
|
14
|
+
'apikey',
|
|
15
|
+
'privatekey',
|
|
16
|
+
'creditcard',
|
|
17
|
+
'cardnumber',
|
|
18
|
+
'cvv',
|
|
19
|
+
'ssn',
|
|
20
|
+
];
|
|
21
|
+
export const REDACTED = '[redacted]';
|
|
22
|
+
const normalize = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
23
|
+
export function isSensitive(key, redactKeys) {
|
|
24
|
+
const name = normalize(key);
|
|
25
|
+
return name.length > 0 && redactKeys.some((pattern) => name.includes(normalize(pattern)));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Turns a request or response body into something safe to log: sensitive
|
|
29
|
+
* fields redacted, binary data and streams summarized, and anything larger
|
|
30
|
+
* than `maxBodyBytes` cut to a text preview.
|
|
31
|
+
*/
|
|
32
|
+
export function captureBody(value, { maxBodyBytes, redactKeys }) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (isBinary(value)) {
|
|
37
|
+
return { size: value.byteLength, summary: `[binary ${formatBytes(value.byteLength)}]` };
|
|
38
|
+
}
|
|
39
|
+
if (isStream(value)) {
|
|
40
|
+
return { size: 0, summary: '[stream]' };
|
|
41
|
+
}
|
|
42
|
+
let json;
|
|
43
|
+
try {
|
|
44
|
+
json = JSON.stringify(value, (key, field) => (key && isSensitive(key, redactKeys) ? REDACTED : field));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { size: 0, summary: '[could not be serialized]' };
|
|
48
|
+
}
|
|
49
|
+
if (json === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const size = Buffer.byteLength(json);
|
|
53
|
+
if (size > maxBodyBytes) {
|
|
54
|
+
return { size, truncated: true, preview: json.slice(0, maxBodyBytes) };
|
|
55
|
+
}
|
|
56
|
+
return { size, value: JSON.parse(json) };
|
|
57
|
+
}
|
|
58
|
+
/** Request headers with sensitive values replaced. */
|
|
59
|
+
export function captureHeaders(headers, redactKeys) {
|
|
60
|
+
const captured = {};
|
|
61
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
captured[name] = isSensitive(name, redactKeys) ? REDACTED : Array.isArray(value) ? value.join(', ') : value;
|
|
66
|
+
}
|
|
67
|
+
return captured;
|
|
68
|
+
}
|
|
69
|
+
/** The body Nest sends for an exception: HttpException's response, or its generic 500. */
|
|
70
|
+
export function errorResponseBody(error) {
|
|
71
|
+
if (error instanceof HttpException) {
|
|
72
|
+
const response = error.getResponse();
|
|
73
|
+
return typeof response === 'string' ? { statusCode: error.getStatus(), message: response } : response;
|
|
74
|
+
}
|
|
75
|
+
return { statusCode: 500, message: 'Internal server error' };
|
|
76
|
+
}
|
|
77
|
+
/** Leaves out empty query and params objects: there's nothing to show. */
|
|
78
|
+
export function nonEmpty(value) {
|
|
79
|
+
if (value && typeof value === 'object' && Object.keys(value).length === 0) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function isBinary(value) {
|
|
85
|
+
return value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
86
|
+
}
|
|
87
|
+
// Readable streams and Nest's StreamableFile.
|
|
88
|
+
function isStream(value) {
|
|
89
|
+
if (!value || typeof value !== 'object') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const candidate = value;
|
|
93
|
+
return typeof candidate.pipe === 'function' || typeof candidate.getStream === 'function';
|
|
94
|
+
}
|
|
95
|
+
function formatBytes(bytes) {
|
|
96
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
|
+
}
|
package/dist/esm/http.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface HttpRequest {
|
|
|
15
15
|
};
|
|
16
16
|
/** Fastify 3. */
|
|
17
17
|
routerPath?: string;
|
|
18
|
+
/** Parsed by Nest's body parser (Express) or by Fastify. */
|
|
19
|
+
body?: unknown;
|
|
20
|
+
query?: unknown;
|
|
21
|
+
params?: unknown;
|
|
18
22
|
}
|
|
19
23
|
export interface HttpResponse {
|
|
20
24
|
statusCode: number;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { NestRnLensModule } from './module.js';
|
|
2
2
|
export { NestRnLensInterceptor } from './interceptor.js';
|
|
3
3
|
export { NEST_RN_LENS_HEADERS } from './types.js';
|
|
4
|
-
export type { NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
|
|
4
|
+
export type { CapturedBody, NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
|
|
@@ -11,6 +11,7 @@ export declare class NestRnLensInterceptor implements NestInterceptor {
|
|
|
11
11
|
private readonly reporter;
|
|
12
12
|
constructor(options: NestRnLensOptions, reporter: NestRnLensReporter);
|
|
13
13
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
14
|
+
private captureOptions;
|
|
14
15
|
/**
|
|
15
16
|
* The status Nest is about to send. Depending on the platform it may not be
|
|
16
17
|
* written to the response yet, so follow Nest's own rules: @HttpCode(),
|
package/dist/esm/interceptor.js
CHANGED
|
@@ -13,6 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
13
13
|
import { HttpException, Inject, Injectable, } from '@nestjs/common';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { tap } from 'rxjs';
|
|
16
|
+
import { captureBody, captureHeaders, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_KEYS, errorResponseBody, nonEmpty, } from './capture.js';
|
|
16
17
|
import { NEST_RN_LENS_OPTIONS } from './constants.js';
|
|
17
18
|
import { detectPlatform, readHeader, requestPath, routePattern, writeHeader, } from './http.js';
|
|
18
19
|
import { NestRnLensReporter } from './reporter.js';
|
|
@@ -43,7 +44,15 @@ let NestRnLensInterceptor = class NestRnLensInterceptor {
|
|
|
43
44
|
const traceId = readHeader(req, NEST_RN_LENS_HEADERS.traceId) ?? randomUUID();
|
|
44
45
|
// Lets the client, and the next service in the chain, reuse the trace id.
|
|
45
46
|
writeHeader(res, NEST_RN_LENS_HEADERS.traceId, traceId);
|
|
46
|
-
|
|
47
|
+
// Captured before the handler runs, in case it mutates the request.
|
|
48
|
+
const capture = this.captureOptions();
|
|
49
|
+
const request = capture && {
|
|
50
|
+
headers: captureHeaders(req.headers, capture.redactKeys),
|
|
51
|
+
query: captureValue(nonEmpty(req.query), capture),
|
|
52
|
+
params: captureValue(nonEmpty(req.params), capture),
|
|
53
|
+
body: captureBody(req.body, capture),
|
|
54
|
+
};
|
|
55
|
+
const finish = (status, error, responseBody) => {
|
|
47
56
|
const event = {
|
|
48
57
|
id: randomUUID(),
|
|
49
58
|
traceId,
|
|
@@ -64,14 +73,22 @@ let NestRnLensInterceptor = class NestRnLensInterceptor {
|
|
|
64
73
|
},
|
|
65
74
|
status,
|
|
66
75
|
error,
|
|
76
|
+
...(capture && { request, response: { body: captureBody(responseBody, capture) } }),
|
|
67
77
|
};
|
|
68
78
|
this.reporter.report(event);
|
|
69
79
|
};
|
|
70
80
|
return next.handle().pipe(tap({
|
|
71
|
-
next: () => finish(this.successStatus(context, req, res)),
|
|
72
|
-
error: (err) => finish(err instanceof HttpException ? err.getStatus() : 500, err instanceof Error ? err.message : String(err)),
|
|
81
|
+
next: (body) => finish(this.successStatus(context, req, res), undefined, body),
|
|
82
|
+
error: (err) => finish(err instanceof HttpException ? err.getStatus() : 500, err instanceof Error ? err.message : String(err), errorResponseBody(err)),
|
|
73
83
|
}));
|
|
74
84
|
}
|
|
85
|
+
captureOptions() {
|
|
86
|
+
const { captureBodies, maxBodyBytes, redactKeys } = this.options;
|
|
87
|
+
if (captureBodies === false) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return { maxBodyBytes: maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES, redactKeys: redactKeys ?? DEFAULT_REDACT_KEYS };
|
|
91
|
+
}
|
|
75
92
|
/**
|
|
76
93
|
* The status Nest is about to send. Depending on the platform it may not be
|
|
77
94
|
* written to the response yet, so follow Nest's own rules: @HttpCode(),
|
|
@@ -95,3 +112,7 @@ NestRnLensInterceptor = __decorate([
|
|
|
95
112
|
__metadata("design:paramtypes", [Object, NestRnLensReporter])
|
|
96
113
|
], NestRnLensInterceptor);
|
|
97
114
|
export { NestRnLensInterceptor };
|
|
115
|
+
/** Small values (query, params) keep their structure; their redacted value is all we need. */
|
|
116
|
+
function captureValue(value, capture) {
|
|
117
|
+
return captureBody(value, capture)?.value;
|
|
118
|
+
}
|
package/dist/esm/module.js
CHANGED
|
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var NestRnLensModule_1;
|
|
8
8
|
import { Module } from '@nestjs/common';
|
|
9
9
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
10
|
+
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_KEYS } from './capture.js';
|
|
10
11
|
import { NEST_RN_LENS_OPTIONS } from './constants.js';
|
|
11
12
|
import { NestRnLensInterceptor } from './interceptor.js';
|
|
12
13
|
import { NestRnLensReporter } from './reporter.js';
|
|
@@ -24,6 +25,9 @@ let NestRnLensModule = NestRnLensModule_1 = class NestRnLensModule {
|
|
|
24
25
|
...options,
|
|
25
26
|
enabled: options.enabled ?? process.env.NODE_ENV !== 'production',
|
|
26
27
|
log: options.log ?? true,
|
|
28
|
+
captureBodies: options.captureBodies ?? true,
|
|
29
|
+
maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
|
|
30
|
+
redactKeys: [...DEFAULT_REDACT_KEYS, ...(options.redactKeys ?? [])],
|
|
27
31
|
};
|
|
28
32
|
return {
|
|
29
33
|
module: NestRnLensModule_1,
|
package/dist/esm/reporter.d.ts
CHANGED
|
@@ -10,3 +10,9 @@ export declare class NestRnLensReporter {
|
|
|
10
10
|
private describe;
|
|
11
11
|
private shortPath;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Callers that are source locations are shown relative to the monorepo root
|
|
15
|
+
* (clients send absolute paths so editors can open them). Anything else, such as
|
|
16
|
+
* a web page path like "/orders/42", is shown as sent.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatCaller(caller: string, repoRoot: string): string;
|
package/dist/esm/reporter.js
CHANGED
|
@@ -45,14 +45,12 @@ let NestRnLensReporter = class NestRnLensReporter {
|
|
|
45
45
|
const from = [`${source.app} (${source.platform})`, this.shortPath(source.caller)].filter(Boolean).join(' ');
|
|
46
46
|
return `${from} → ${target.method} ${target.route} → ${target.controller}.${target.handler} ${status} ${durationMs}ms`;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!file?.startsWith('/')) {
|
|
52
|
-
return file;
|
|
48
|
+
shortPath(caller) {
|
|
49
|
+
if (!caller || !SOURCE_LOCATION.test(caller)) {
|
|
50
|
+
return caller;
|
|
53
51
|
}
|
|
54
52
|
this.repoRoot ??= findRepoRoot(process.cwd());
|
|
55
|
-
return
|
|
53
|
+
return formatCaller(caller, this.repoRoot);
|
|
56
54
|
}
|
|
57
55
|
};
|
|
58
56
|
NestRnLensReporter = __decorate([
|
|
@@ -61,6 +59,16 @@ NestRnLensReporter = __decorate([
|
|
|
61
59
|
__metadata("design:paramtypes", [Object])
|
|
62
60
|
], NestRnLensReporter);
|
|
63
61
|
export { NestRnLensReporter };
|
|
62
|
+
// "/repo/apps/mobile/src/screens/Orders.tsx:23": an absolute file path plus a line.
|
|
63
|
+
const SOURCE_LOCATION = /^\/.+:\d+$/;
|
|
64
|
+
/**
|
|
65
|
+
* Callers that are source locations are shown relative to the monorepo root
|
|
66
|
+
* (clients send absolute paths so editors can open them). Anything else, such as
|
|
67
|
+
* a web page path like "/orders/42", is shown as sent.
|
|
68
|
+
*/
|
|
69
|
+
export function formatCaller(caller, repoRoot) {
|
|
70
|
+
return SOURCE_LOCATION.test(caller) ? relative(repoRoot, caller) : caller;
|
|
71
|
+
}
|
|
64
72
|
const ROOT_MARKERS = ['turbo.json', 'pnpm-workspace.yaml', '.git'];
|
|
65
73
|
function findRepoRoot(from) {
|
|
66
74
|
let dir = from;
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -11,6 +11,18 @@ export declare const NEST_RN_LENS_HEADERS: {
|
|
|
11
11
|
readonly traceId: "x-nest-rn-lens-trace-id";
|
|
12
12
|
};
|
|
13
13
|
export type Platform = 'ios' | 'android' | 'web' | 'unknown';
|
|
14
|
+
/** A request or response body, made safe to log. */
|
|
15
|
+
export interface CapturedBody {
|
|
16
|
+
/** Size of the serialized body, in bytes. */
|
|
17
|
+
size: number;
|
|
18
|
+
/** The body, with sensitive fields redacted. Absent when truncated or summarized. */
|
|
19
|
+
value?: unknown;
|
|
20
|
+
/** Set when the body was larger than `maxBodyBytes`; `preview` holds its start. */
|
|
21
|
+
truncated?: boolean;
|
|
22
|
+
preview?: string;
|
|
23
|
+
/** For bodies that aren't JSON: "[binary 12.0 KB]", "[stream]". */
|
|
24
|
+
summary?: string;
|
|
25
|
+
}
|
|
14
26
|
/** One request, as seen by the API. */
|
|
15
27
|
export interface NestRnLensEvent {
|
|
16
28
|
id: string;
|
|
@@ -40,6 +52,20 @@ export interface NestRnLensEvent {
|
|
|
40
52
|
status: number;
|
|
41
53
|
/** Message of the exception, when the handler threw. */
|
|
42
54
|
error?: string;
|
|
55
|
+
/** What the client sent. Only present when `captureBodies` is on (the default). */
|
|
56
|
+
request?: {
|
|
57
|
+
/** Request headers; sensitive ones redacted. */
|
|
58
|
+
headers: Record<string, string>;
|
|
59
|
+
query?: unknown;
|
|
60
|
+
/** Route parameters, e.g. { id: "42" } for /orders/:id. */
|
|
61
|
+
params?: unknown;
|
|
62
|
+
body?: CapturedBody;
|
|
63
|
+
};
|
|
64
|
+
/** What the API answered. Only present when `captureBodies` is on (the default). */
|
|
65
|
+
response?: {
|
|
66
|
+
/** The handler's return value, or the error body Nest sends for an exception. */
|
|
67
|
+
body?: CapturedBody;
|
|
68
|
+
};
|
|
43
69
|
}
|
|
44
70
|
export interface NestRnLensOptions {
|
|
45
71
|
/** Name of this API in the traffic graph, e.g. "api". */
|
|
@@ -59,4 +85,19 @@ export interface NestRnLensOptions {
|
|
|
59
85
|
* debug level that the NestRN Lens VS Code extension reads). Defaults to `true`.
|
|
60
86
|
*/
|
|
61
87
|
log?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Include request and response bodies, query, route params and request
|
|
90
|
+
* headers in each event. Defaults to `true`. Sensitive fields are redacted
|
|
91
|
+
* (see `redactKeys`).
|
|
92
|
+
*/
|
|
93
|
+
captureBodies?: boolean;
|
|
94
|
+
/** Bodies larger than this are cut to a preview. Defaults to 16 KB. */
|
|
95
|
+
maxBodyBytes?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Extra field or header names to redact, added to the defaults (password,
|
|
98
|
+
* secret, token, authorization, cookie, apiKey, privateKey, creditCard,
|
|
99
|
+
* cardNumber, cvv, ssn). Matched ignoring case and separators, anywhere in
|
|
100
|
+
* the name: "token" also covers "accessToken".
|
|
101
|
+
*/
|
|
102
|
+
redactKeys?: string[];
|
|
62
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nest-rn-lens/nest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "NestJS interceptor that reports every request together with the React Native screen that made it. Part of NestRN Lens.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nestjs",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"monorepo",
|
|
13
13
|
"observability",
|
|
14
14
|
"tracing",
|
|
15
|
-
"devtools"
|
|
15
|
+
"devtools",
|
|
16
|
+
"nextjs"
|
|
16
17
|
],
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"author": "isa",
|