@nest-rn-lens/nest 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,199 @@
1
- # Temporary Holding Version
1
+ # @nest-rn-lens/nest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **See which React Native screen made each request to your NestJS API.**
4
+
5
+ A NestJS interceptor that records every request your API handles: the app and
6
+ the exact file and line that sent it, the endpoint and the handler that answered,
7
+ the status, and the time it took.
8
+
9
+ ```
10
+ [NestRnLens] mobile (ios) apps/mobile/src/screens/order-details.tsx:23 → GET /orders/:id → OrdersController.findOne 200 4ms
11
+ ```
12
+
13
+ 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
+ request logs and a typed event for every call. With the NestRN Lens VS Code
16
+ extension, those events become a live traffic panel where you can click any
17
+ request to open the screen that made it or the handler that answered it.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @nest-rn-lens/nest
23
+ # or
24
+ pnpm add @nest-rn-lens/nest
25
+ yarn add @nest-rn-lens/nest
26
+ ```
27
+
28
+ ## Requirements
29
+
30
+ Any NestJS API that serves React Native (or web) clients works. There's nothing
31
+ specific to one project.
32
+
33
+ | Requirement | Supported |
34
+ | ---------------- | ----------------------------------------------- |
35
+ | NestJS | 10, 11 or 12 |
36
+ | RxJS | 7 |
37
+ | HTTP platform | Express (default) or Fastify |
38
+ | Module system | ES modules or CommonJS |
39
+ | Node.js | 18+, or what your NestJS version requires |
40
+
41
+ A Turborepo isn't required for the interceptor itself. It's only needed for the
42
+ NestRN Lens VS Code extension, which finds your API and app inside the monorepo.
43
+
44
+ ## Quick start
45
+
46
+ Import the module once, in your root module:
47
+
48
+ ```ts
49
+ import { Module } from '@nestjs/common';
50
+ import { NestRnLensModule } from '@nest-rn-lens/nest';
51
+
52
+ @Module({
53
+ imports: [NestRnLensModule.forRoot({ app: 'api' })],
54
+ })
55
+ export class AppModule {}
56
+ ```
57
+
58
+ That's it. Every request handled by a controller is now logged:
59
+
60
+ ```
61
+ [NestRnLens] unknown (android) → GET /orders → OrdersController.findAll 200 3ms
62
+ ```
63
+
64
+ Requests show `unknown` until the client says who it is. The next section
65
+ explains how.
66
+
67
+ ## Telling the API who's calling
68
+
69
+ A request on its own doesn't say which app or which screen sent it. The client
70
+ adds that with three headers:
71
+
72
+ | Header | Example | Used for |
73
+ | ------------------------- | ----------------------------------------------- | ------------------------------------------------- |
74
+ | `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 |
76
+ | `x-nest-rn-lens-trace-id` | `5f0c…` | Linking every hop of one request chain |
77
+
78
+ You don't have to write these by hand. The upcoming React Native client,
79
+ `@nest-rn-lens/react-native`, wraps `fetch` and fills them in development,
80
+ working out the calling file from the stack trace.
81
+
82
+ When a header is missing, the interceptor still reports the request:
83
+
84
+ - the app is `unknown`
85
+ - the platform is guessed from the `User-Agent` (`okhttp` → Android,
86
+ `CFNetwork` → iOS, a browser → web)
87
+ - a new trace id is created
88
+
89
+ The trace id is always sent back in the `x-nest-rn-lens-trace-id` response
90
+ header, so the client or the next service can reuse it.
91
+
92
+ The header names are exported, so you can use them in your own code:
93
+
94
+ ```ts
95
+ import { NEST_RN_LENS_HEADERS } from '@nest-rn-lens/nest';
96
+
97
+ fetch(url, { headers: { [NEST_RN_LENS_HEADERS.app]: 'admin-dashboard' } });
98
+ ```
99
+
100
+ ## Options
101
+
102
+ ```ts
103
+ NestRnLensModule.forRoot({
104
+ app: 'api',
105
+ enabled: true,
106
+ log: true,
107
+ onEvent: (event) => {},
108
+ });
109
+ ```
110
+
111
+ | Option | Type | Default | Description |
112
+ | --------- | --------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------- |
113
+ | `app` | `string` | required | Name of this API in logs and in the traffic graph. |
114
+ | `enabled` | `boolean` | `true` unless `NODE_ENV=production` | Turns the interceptor on or off. When off, requests pass through untouched. |
115
+ | `log` | `boolean` | `true` | Logs a readable line per request, plus the full event as JSON at debug level. |
116
+ | `onEvent` | `(event: NestRnLensEvent) => void` | none | Called with every event. Forward traffic to your own tooling, tests or metrics. |
117
+
118
+ ### The event
119
+
120
+ ```ts
121
+ interface NestRnLensEvent {
122
+ id: string;
123
+ traceId: string;
124
+ timestamp: number; // ms since epoch, when the response was ready
125
+ durationMs: number;
126
+ source: {
127
+ app: string; // "mobile", or "unknown"
128
+ caller?: string; // "/repo/apps/mobile/src/screens/orders.tsx:23"
129
+ platform: 'ios' | 'android' | 'web' | 'unknown';
130
+ };
131
+ target: {
132
+ app: string; // your `app` option
133
+ method: string; // "GET"
134
+ path: string; // "/orders/42" (no query string)
135
+ route: string; // "/orders/:id"
136
+ controller: string; // "OrdersController"
137
+ handler: string; // "findOne"
138
+ };
139
+ status: number; // 200, 201, 404, 500…
140
+ error?: string; // exception message, when the handler threw
141
+ }
142
+ ```
143
+
144
+ `route` is the route pattern, not the URL, so all calls to one endpoint group
145
+ together. `status` follows Nest's rules: `@HttpCode()` when set, `201` for
146
+ `POST`, the exception's status when a handler throws, and `500` for other errors.
147
+
148
+ ## Safe by design
149
+
150
+ NestRN Lens is a development tool. It is built so that it can't hurt the API it
151
+ watches:
152
+
153
+ - **Off in production by default.** Unless you pass `enabled: true`, nothing
154
+ runs when `NODE_ENV=production`.
155
+ - **Never breaks a request.** If your `onEvent` callback or the logger throws,
156
+ the error is swallowed and the response goes out as usual.
157
+ - **Doesn't change responses.** The only thing it adds is the trace id header.
158
+ - **HTTP only.** Microservice, WebSocket and GraphQL contexts pass straight
159
+ through.
160
+
161
+ ## Browsers and CORS
162
+
163
+ Requests from a web build carry custom headers, so the browser sends a
164
+ preflight request first. Enable CORS in development:
165
+
166
+ ```ts
167
+ const app = await NestFactory.create(AppModule);
168
+ app.enableCors({ exposedHeaders: ['x-nest-rn-lens-trace-id'] });
169
+ ```
170
+
171
+ `exposedHeaders` is only needed if browser code reads the trace id from the
172
+ response. Native React Native apps don't use CORS at all.
173
+
174
+ ## Limitations
175
+
176
+ - Only requests that reach a controller are reported. A request to a route that
177
+ doesn't exist gets a 404 from Nest before any interceptor runs.
178
+ - `durationMs` is measured inside Nest, from the interceptor until the handler
179
+ returns. It doesn't include network time or response serialization.
180
+
181
+ ## NestRN Lens packages
182
+
183
+ | Package | What it does | Status |
184
+ | ----------------------------- | ----------------------------------------------------------------- | ------------- |
185
+ | `@nest-rn-lens/nest` | This interceptor | Available |
186
+ | `@nest-rn-lens/react-native` | `fetch` wrapper that sends the app name and the calling screen | Coming soon |
187
+ | NestRN Lens for VS Code | Live traffic panel with the app running in a phone frame | Coming soon |
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ npm install
193
+ npm test # builds the tests and runs them on Express and Fastify
194
+ npm run build # ES module and CommonJS builds in dist/
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,4 @@
1
+ /** Injection token for the resolved module options. */
2
+ export declare const NEST_RN_LENS_OPTIONS: unique symbol;
3
+ /** Logger context. The VS Code extension looks for "[NestRnLens] {json}" lines. */
4
+ export declare const LOGGER_CONTEXT = "NestRnLens";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOGGER_CONTEXT = exports.NEST_RN_LENS_OPTIONS = void 0;
4
+ /** Injection token for the resolved module options. */
5
+ exports.NEST_RN_LENS_OPTIONS = Symbol('NEST_RN_LENS_OPTIONS');
6
+ /** Logger context. The VS Code extension looks for "[NestRnLens] {json}" lines. */
7
+ exports.LOGGER_CONTEXT = 'NestRnLens';
@@ -0,0 +1,35 @@
1
+ import type { Platform } from './types.js';
2
+ export interface HttpRequest {
3
+ method: string;
4
+ url: string;
5
+ originalUrl?: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ /** Express: the matched route. */
8
+ route?: {
9
+ path?: unknown;
10
+ };
11
+ baseUrl?: string;
12
+ /** Fastify 4+: the matched route. */
13
+ routeOptions?: {
14
+ url?: string;
15
+ };
16
+ /** Fastify 3. */
17
+ routerPath?: string;
18
+ }
19
+ export interface HttpResponse {
20
+ statusCode: number;
21
+ /** Express (Node's ServerResponse). */
22
+ setHeader?(name: string, value: string): unknown;
23
+ /** Fastify reply. */
24
+ header?(name: string, value: string): unknown;
25
+ }
26
+ export declare function readHeader(req: HttpRequest, name: string): string | undefined;
27
+ export declare function writeHeader(res: HttpResponse, name: string, value: string): void;
28
+ export declare function requestPath(req: HttpRequest): string;
29
+ /** "/orders/:id" rather than "/orders/42", so one endpoint is one graph edge. */
30
+ export declare function routePattern(req: HttpRequest): string;
31
+ /**
32
+ * Best guess when the client doesn't say. React Native's fetch uses okhttp on
33
+ * Android and CFNetwork on iOS; browsers send a Mozilla user agent.
34
+ */
35
+ export declare function detectPlatform(userAgent?: string): Platform;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readHeader = readHeader;
4
+ exports.writeHeader = writeHeader;
5
+ exports.requestPath = requestPath;
6
+ exports.routePattern = routePattern;
7
+ exports.detectPlatform = detectPlatform;
8
+ function readHeader(req, name) {
9
+ const value = req.headers[name];
10
+ return Array.isArray(value) ? value[0] : value;
11
+ }
12
+ function writeHeader(res, name, value) {
13
+ if (typeof res.header === 'function') {
14
+ res.header(name, value);
15
+ }
16
+ else {
17
+ res.setHeader?.(name, value);
18
+ }
19
+ }
20
+ function requestPath(req) {
21
+ return (req.originalUrl ?? req.url).split('?')[0];
22
+ }
23
+ /** "/orders/:id" rather than "/orders/42", so one endpoint is one graph edge. */
24
+ function routePattern(req) {
25
+ const expressRoute = req.route?.path;
26
+ if (typeof expressRoute === 'string') {
27
+ return `${req.baseUrl ?? ''}${expressRoute}`;
28
+ }
29
+ return req.routeOptions?.url ?? req.routerPath ?? requestPath(req);
30
+ }
31
+ /**
32
+ * Best guess when the client doesn't say. React Native's fetch uses okhttp on
33
+ * Android and CFNetwork on iOS; browsers send a Mozilla user agent.
34
+ */
35
+ function detectPlatform(userAgent = '') {
36
+ if (/okhttp|android/i.test(userAgent)) {
37
+ return 'android';
38
+ }
39
+ if (/CFNetwork|Darwin|iPhone|iPad/i.test(userAgent)) {
40
+ return 'ios';
41
+ }
42
+ if (/Mozilla/i.test(userAgent)) {
43
+ return 'web';
44
+ }
45
+ return 'unknown';
46
+ }
@@ -0,0 +1,4 @@
1
+ export { NestRnLensModule } from './module.js';
2
+ export { NestRnLensInterceptor } from './interceptor.js';
3
+ export { NEST_RN_LENS_HEADERS } from './types.js';
4
+ export type { NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEST_RN_LENS_HEADERS = exports.NestRnLensInterceptor = exports.NestRnLensModule = void 0;
4
+ var module_js_1 = require("./module.js");
5
+ Object.defineProperty(exports, "NestRnLensModule", { enumerable: true, get: function () { return module_js_1.NestRnLensModule; } });
6
+ var interceptor_js_1 = require("./interceptor.js");
7
+ Object.defineProperty(exports, "NestRnLensInterceptor", { enumerable: true, get: function () { return interceptor_js_1.NestRnLensInterceptor; } });
8
+ var types_js_1 = require("./types.js");
9
+ Object.defineProperty(exports, "NEST_RN_LENS_HEADERS", { enumerable: true, get: function () { return types_js_1.NEST_RN_LENS_HEADERS; } });
@@ -0,0 +1,20 @@
1
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
2
+ import { type Observable } from 'rxjs';
3
+ import { NestRnLensReporter } from './reporter.js';
4
+ import { type NestRnLensOptions } from './types.js';
5
+ /**
6
+ * Records every HTTP request handled by a controller: who called it (from the
7
+ * x-nest-rn-lens-* headers), which handler answered, the status and the time.
8
+ */
9
+ export declare class NestRnLensInterceptor implements NestInterceptor {
10
+ private readonly options;
11
+ private readonly reporter;
12
+ constructor(options: NestRnLensOptions, reporter: NestRnLensReporter);
13
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
14
+ /**
15
+ * The status Nest is about to send. Depending on the platform it may not be
16
+ * written to the response yet, so follow Nest's own rules: @HttpCode(),
17
+ * otherwise 201 for POST and 200 for everything else.
18
+ */
19
+ private successStatus;
20
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.NestRnLensInterceptor = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_crypto_1 = require("node:crypto");
18
+ const rxjs_1 = require("rxjs");
19
+ const constants_js_1 = require("./constants.js");
20
+ const http_js_1 = require("./http.js");
21
+ const reporter_js_1 = require("./reporter.js");
22
+ const types_js_1 = require("./types.js");
23
+ // Metadata key behind @HttpCode() (HTTP_CODE_METADATA in @nestjs/common/constants,
24
+ // unchanged since Nest 5). Inlined because that subpath resolves differently
25
+ // across Nest versions.
26
+ const HTTP_CODE_METADATA = '__httpCode__';
27
+ /**
28
+ * Records every HTTP request handled by a controller: who called it (from the
29
+ * x-nest-rn-lens-* headers), which handler answered, the status and the time.
30
+ */
31
+ let NestRnLensInterceptor = class NestRnLensInterceptor {
32
+ options;
33
+ reporter;
34
+ constructor(options, reporter) {
35
+ this.options = options;
36
+ this.reporter = reporter;
37
+ }
38
+ intercept(context, next) {
39
+ if (!this.options.enabled || context.getType() !== 'http') {
40
+ return next.handle();
41
+ }
42
+ const http = context.switchToHttp();
43
+ const req = http.getRequest();
44
+ const res = http.getResponse();
45
+ const start = performance.now();
46
+ const traceId = (0, http_js_1.readHeader)(req, types_js_1.NEST_RN_LENS_HEADERS.traceId) ?? (0, node_crypto_1.randomUUID)();
47
+ // Lets the client, and the next service in the chain, reuse the trace id.
48
+ (0, http_js_1.writeHeader)(res, types_js_1.NEST_RN_LENS_HEADERS.traceId, traceId);
49
+ const finish = (status, error) => {
50
+ const event = {
51
+ id: (0, node_crypto_1.randomUUID)(),
52
+ traceId,
53
+ timestamp: Date.now(),
54
+ durationMs: Math.round(performance.now() - start),
55
+ source: {
56
+ app: (0, http_js_1.readHeader)(req, types_js_1.NEST_RN_LENS_HEADERS.app) ?? 'unknown',
57
+ caller: (0, http_js_1.readHeader)(req, types_js_1.NEST_RN_LENS_HEADERS.caller),
58
+ platform: (0, http_js_1.detectPlatform)((0, http_js_1.readHeader)(req, 'user-agent')),
59
+ },
60
+ target: {
61
+ app: this.options.app,
62
+ method: req.method,
63
+ path: (0, http_js_1.requestPath)(req),
64
+ route: (0, http_js_1.routePattern)(req),
65
+ controller: context.getClass().name,
66
+ handler: context.getHandler().name,
67
+ },
68
+ status,
69
+ error,
70
+ };
71
+ this.reporter.report(event);
72
+ };
73
+ 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)),
76
+ }));
77
+ }
78
+ /**
79
+ * The status Nest is about to send. Depending on the platform it may not be
80
+ * written to the response yet, so follow Nest's own rules: @HttpCode(),
81
+ * otherwise 201 for POST and 200 for everything else.
82
+ */
83
+ successStatus(context, req, res) {
84
+ const declared = Reflect.getMetadata(HTTP_CODE_METADATA, context.getHandler());
85
+ if (declared) {
86
+ return declared;
87
+ }
88
+ // A handler using @Res() may have set its own status.
89
+ if (res.statusCode && res.statusCode !== 200) {
90
+ return res.statusCode;
91
+ }
92
+ return req.method === 'POST' ? 201 : 200;
93
+ }
94
+ };
95
+ exports.NestRnLensInterceptor = NestRnLensInterceptor;
96
+ exports.NestRnLensInterceptor = NestRnLensInterceptor = __decorate([
97
+ (0, common_1.Injectable)(),
98
+ __param(0, (0, common_1.Inject)(constants_js_1.NEST_RN_LENS_OPTIONS)),
99
+ __metadata("design:paramtypes", [Object, reporter_js_1.NestRnLensReporter])
100
+ ], NestRnLensInterceptor);
@@ -0,0 +1,13 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import type { NestRnLensOptions } from './types.js';
3
+ export declare class NestRnLensModule {
4
+ /**
5
+ * Registers the interceptor for every controller in the app.
6
+ *
7
+ * ```ts
8
+ * @Module({ imports: [NestRnLensModule.forRoot({ app: 'api' })] })
9
+ * export class AppModule {}
10
+ * ```
11
+ */
12
+ static forRoot(options: NestRnLensOptions): DynamicModule;
13
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var NestRnLensModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.NestRnLensModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const core_1 = require("@nestjs/core");
13
+ const constants_js_1 = require("./constants.js");
14
+ const interceptor_js_1 = require("./interceptor.js");
15
+ const reporter_js_1 = require("./reporter.js");
16
+ let NestRnLensModule = NestRnLensModule_1 = class NestRnLensModule {
17
+ /**
18
+ * Registers the interceptor for every controller in the app.
19
+ *
20
+ * ```ts
21
+ * @Module({ imports: [NestRnLensModule.forRoot({ app: 'api' })] })
22
+ * export class AppModule {}
23
+ * ```
24
+ */
25
+ static forRoot(options) {
26
+ const resolved = {
27
+ ...options,
28
+ enabled: options.enabled ?? process.env.NODE_ENV !== 'production',
29
+ log: options.log ?? true,
30
+ };
31
+ return {
32
+ module: NestRnLensModule_1,
33
+ providers: [
34
+ { provide: constants_js_1.NEST_RN_LENS_OPTIONS, useValue: resolved },
35
+ reporter_js_1.NestRnLensReporter,
36
+ { provide: core_1.APP_INTERCEPTOR, useClass: interceptor_js_1.NestRnLensInterceptor },
37
+ ],
38
+ };
39
+ }
40
+ };
41
+ exports.NestRnLensModule = NestRnLensModule;
42
+ exports.NestRnLensModule = NestRnLensModule = NestRnLensModule_1 = __decorate([
43
+ (0, common_1.Module)({})
44
+ ], NestRnLensModule);
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,12 @@
1
+ import type { NestRnLensEvent, NestRnLensOptions } from './types.js';
2
+ /** Sends each event to the logger and to the `onEvent` callback. Never throws. */
3
+ export declare class NestRnLensReporter {
4
+ private readonly options;
5
+ private readonly logger;
6
+ private repoRoot?;
7
+ constructor(options: NestRnLensOptions);
8
+ report(event: NestRnLensEvent): void;
9
+ /** "mobile (ios) apps/mobile/src/screens/orders.tsx:17 → GET /orders → OrdersController.findAll 200 4ms" */
10
+ private describe;
11
+ private shortPath;
12
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.NestRnLensReporter = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
19
+ const constants_js_1 = require("./constants.js");
20
+ /** Sends each event to the logger and to the `onEvent` callback. Never throws. */
21
+ let NestRnLensReporter = class NestRnLensReporter {
22
+ options;
23
+ logger = new common_1.Logger(constants_js_1.LOGGER_CONTEXT);
24
+ repoRoot;
25
+ constructor(options) {
26
+ this.options = options;
27
+ }
28
+ report(event) {
29
+ try {
30
+ this.options.onEvent?.(event);
31
+ }
32
+ catch {
33
+ // A broken callback must not break the request it observes.
34
+ }
35
+ if (this.options.log === false) {
36
+ return;
37
+ }
38
+ try {
39
+ this.logger.log(this.describe(event));
40
+ this.logger.debug(JSON.stringify(event));
41
+ }
42
+ catch {
43
+ // Same for a broken logger.
44
+ }
45
+ }
46
+ /** "mobile (ios) apps/mobile/src/screens/orders.tsx:17 → GET /orders → OrdersController.findAll 200 4ms" */
47
+ describe({ source, target, status, durationMs }) {
48
+ const from = [`${source.app} (${source.platform})`, this.shortPath(source.caller)].filter(Boolean).join(' ');
49
+ return `${from} → ${target.method} ${target.route} → ${target.controller}.${target.handler} ${status} ${durationMs}ms`;
50
+ }
51
+ // Clients send absolute paths (editors need them to open the file); logs are
52
+ // easier to read relative to the monorepo root.
53
+ shortPath(file) {
54
+ if (!file?.startsWith('/')) {
55
+ return file;
56
+ }
57
+ this.repoRoot ??= findRepoRoot(process.cwd());
58
+ return (0, node_path_1.relative)(this.repoRoot, file);
59
+ }
60
+ };
61
+ exports.NestRnLensReporter = NestRnLensReporter;
62
+ exports.NestRnLensReporter = NestRnLensReporter = __decorate([
63
+ (0, common_1.Injectable)(),
64
+ __param(0, (0, common_1.Inject)(constants_js_1.NEST_RN_LENS_OPTIONS)),
65
+ __metadata("design:paramtypes", [Object])
66
+ ], NestRnLensReporter);
67
+ const ROOT_MARKERS = ['turbo.json', 'pnpm-workspace.yaml', '.git'];
68
+ function findRepoRoot(from) {
69
+ let dir = from;
70
+ while (!ROOT_MARKERS.some((marker) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, marker)))) {
71
+ const parent = (0, node_path_1.dirname)(dir);
72
+ if (parent === dir) {
73
+ return from;
74
+ }
75
+ dir = parent;
76
+ }
77
+ return dir;
78
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Headers a client sends so the interceptor knows who made the request.
3
+ * The React Native client (@nest-rn-lens/react-native) fills them in dev.
4
+ */
5
+ export declare const NEST_RN_LENS_HEADERS: {
6
+ /** Name of the calling app, e.g. "mobile". */
7
+ readonly app: "x-nest-rn-lens-app";
8
+ /** Where in the client the call was made, e.g. "/…/src/screens/OrderList.tsx:42". */
9
+ readonly caller: "x-nest-rn-lens-caller";
10
+ /** Same id across every hop of one request chain. Echoed back on the response. */
11
+ readonly traceId: "x-nest-rn-lens-trace-id";
12
+ };
13
+ export type Platform = 'ios' | 'android' | 'web' | 'unknown';
14
+ /** One request, as seen by the API. */
15
+ export interface NestRnLensEvent {
16
+ id: string;
17
+ traceId: string;
18
+ /** When the response was ready (ms since epoch). */
19
+ timestamp: number;
20
+ durationMs: number;
21
+ source: {
22
+ /** From the x-nest-rn-lens-app header, or "unknown". */
23
+ app: string;
24
+ /** From the x-nest-rn-lens-caller header. */
25
+ caller?: string;
26
+ /** Guessed from the User-Agent. */
27
+ platform: Platform;
28
+ };
29
+ target: {
30
+ /** The `app` option of this API. */
31
+ app: string;
32
+ method: string;
33
+ /** Actual URL path without the query string, e.g. "/orders/42". */
34
+ path: string;
35
+ /** Route pattern, e.g. "/orders/:id". Groups requests to the same endpoint. */
36
+ route: string;
37
+ controller: string;
38
+ handler: string;
39
+ };
40
+ status: number;
41
+ /** Message of the exception, when the handler threw. */
42
+ error?: string;
43
+ }
44
+ export interface NestRnLensOptions {
45
+ /** Name of this API in the traffic graph, e.g. "api". */
46
+ app: string;
47
+ /**
48
+ * Turns reporting on or off. Defaults to `true` unless NODE_ENV is
49
+ * "production", so nothing runs in production unless you opt in.
50
+ */
51
+ enabled?: boolean;
52
+ /**
53
+ * Called with every event. Use it to forward traffic somewhere else.
54
+ * Errors thrown here are ignored, so they never affect the request.
55
+ */
56
+ onEvent?: (event: NestRnLensEvent) => void;
57
+ /**
58
+ * Log every event through Nest's Logger (a readable line, plus the JSON at
59
+ * debug level that the NestRN Lens VS Code extension reads). Defaults to `true`.
60
+ */
61
+ log?: boolean;
62
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEST_RN_LENS_HEADERS = void 0;
4
+ /**
5
+ * Headers a client sends so the interceptor knows who made the request.
6
+ * The React Native client (@nest-rn-lens/react-native) fills them in dev.
7
+ */
8
+ exports.NEST_RN_LENS_HEADERS = {
9
+ /** Name of the calling app, e.g. "mobile". */
10
+ app: 'x-nest-rn-lens-app',
11
+ /** Where in the client the call was made, e.g. "/…/src/screens/OrderList.tsx:42". */
12
+ caller: 'x-nest-rn-lens-caller',
13
+ /** Same id across every hop of one request chain. Echoed back on the response. */
14
+ traceId: 'x-nest-rn-lens-trace-id',
15
+ };
@@ -0,0 +1,4 @@
1
+ /** Injection token for the resolved module options. */
2
+ export declare const NEST_RN_LENS_OPTIONS: unique symbol;
3
+ /** Logger context. The VS Code extension looks for "[NestRnLens] {json}" lines. */
4
+ export declare const LOGGER_CONTEXT = "NestRnLens";
@@ -0,0 +1,4 @@
1
+ /** Injection token for the resolved module options. */
2
+ export const NEST_RN_LENS_OPTIONS = Symbol('NEST_RN_LENS_OPTIONS');
3
+ /** Logger context. The VS Code extension looks for "[NestRnLens] {json}" lines. */
4
+ export const LOGGER_CONTEXT = 'NestRnLens';
@@ -0,0 +1,35 @@
1
+ import type { Platform } from './types.js';
2
+ export interface HttpRequest {
3
+ method: string;
4
+ url: string;
5
+ originalUrl?: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ /** Express: the matched route. */
8
+ route?: {
9
+ path?: unknown;
10
+ };
11
+ baseUrl?: string;
12
+ /** Fastify 4+: the matched route. */
13
+ routeOptions?: {
14
+ url?: string;
15
+ };
16
+ /** Fastify 3. */
17
+ routerPath?: string;
18
+ }
19
+ export interface HttpResponse {
20
+ statusCode: number;
21
+ /** Express (Node's ServerResponse). */
22
+ setHeader?(name: string, value: string): unknown;
23
+ /** Fastify reply. */
24
+ header?(name: string, value: string): unknown;
25
+ }
26
+ export declare function readHeader(req: HttpRequest, name: string): string | undefined;
27
+ export declare function writeHeader(res: HttpResponse, name: string, value: string): void;
28
+ export declare function requestPath(req: HttpRequest): string;
29
+ /** "/orders/:id" rather than "/orders/42", so one endpoint is one graph edge. */
30
+ export declare function routePattern(req: HttpRequest): string;
31
+ /**
32
+ * Best guess when the client doesn't say. React Native's fetch uses okhttp on
33
+ * Android and CFNetwork on iOS; browsers send a Mozilla user agent.
34
+ */
35
+ export declare function detectPlatform(userAgent?: string): Platform;
@@ -0,0 +1,39 @@
1
+ export function readHeader(req, name) {
2
+ const value = req.headers[name];
3
+ return Array.isArray(value) ? value[0] : value;
4
+ }
5
+ export function writeHeader(res, name, value) {
6
+ if (typeof res.header === 'function') {
7
+ res.header(name, value);
8
+ }
9
+ else {
10
+ res.setHeader?.(name, value);
11
+ }
12
+ }
13
+ export function requestPath(req) {
14
+ return (req.originalUrl ?? req.url).split('?')[0];
15
+ }
16
+ /** "/orders/:id" rather than "/orders/42", so one endpoint is one graph edge. */
17
+ export function routePattern(req) {
18
+ const expressRoute = req.route?.path;
19
+ if (typeof expressRoute === 'string') {
20
+ return `${req.baseUrl ?? ''}${expressRoute}`;
21
+ }
22
+ return req.routeOptions?.url ?? req.routerPath ?? requestPath(req);
23
+ }
24
+ /**
25
+ * Best guess when the client doesn't say. React Native's fetch uses okhttp on
26
+ * Android and CFNetwork on iOS; browsers send a Mozilla user agent.
27
+ */
28
+ export function detectPlatform(userAgent = '') {
29
+ if (/okhttp|android/i.test(userAgent)) {
30
+ return 'android';
31
+ }
32
+ if (/CFNetwork|Darwin|iPhone|iPad/i.test(userAgent)) {
33
+ return 'ios';
34
+ }
35
+ if (/Mozilla/i.test(userAgent)) {
36
+ return 'web';
37
+ }
38
+ return 'unknown';
39
+ }
@@ -0,0 +1,4 @@
1
+ export { NestRnLensModule } from './module.js';
2
+ export { NestRnLensInterceptor } from './interceptor.js';
3
+ export { NEST_RN_LENS_HEADERS } from './types.js';
4
+ export type { NestRnLensEvent, NestRnLensOptions, Platform } from './types.js';
@@ -0,0 +1,3 @@
1
+ export { NestRnLensModule } from './module.js';
2
+ export { NestRnLensInterceptor } from './interceptor.js';
3
+ export { NEST_RN_LENS_HEADERS } from './types.js';
@@ -0,0 +1,20 @@
1
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
2
+ import { type Observable } from 'rxjs';
3
+ import { NestRnLensReporter } from './reporter.js';
4
+ import { type NestRnLensOptions } from './types.js';
5
+ /**
6
+ * Records every HTTP request handled by a controller: who called it (from the
7
+ * x-nest-rn-lens-* headers), which handler answered, the status and the time.
8
+ */
9
+ export declare class NestRnLensInterceptor implements NestInterceptor {
10
+ private readonly options;
11
+ private readonly reporter;
12
+ constructor(options: NestRnLensOptions, reporter: NestRnLensReporter);
13
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
14
+ /**
15
+ * The status Nest is about to send. Depending on the platform it may not be
16
+ * written to the response yet, so follow Nest's own rules: @HttpCode(),
17
+ * otherwise 201 for POST and 200 for everything else.
18
+ */
19
+ private successStatus;
20
+ }
@@ -0,0 +1,97 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { HttpException, Inject, Injectable, } from '@nestjs/common';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { tap } from 'rxjs';
16
+ import { NEST_RN_LENS_OPTIONS } from './constants.js';
17
+ import { detectPlatform, readHeader, requestPath, routePattern, writeHeader, } from './http.js';
18
+ import { NestRnLensReporter } from './reporter.js';
19
+ import { NEST_RN_LENS_HEADERS } from './types.js';
20
+ // Metadata key behind @HttpCode() (HTTP_CODE_METADATA in @nestjs/common/constants,
21
+ // unchanged since Nest 5). Inlined because that subpath resolves differently
22
+ // across Nest versions.
23
+ const HTTP_CODE_METADATA = '__httpCode__';
24
+ /**
25
+ * Records every HTTP request handled by a controller: who called it (from the
26
+ * x-nest-rn-lens-* headers), which handler answered, the status and the time.
27
+ */
28
+ let NestRnLensInterceptor = class NestRnLensInterceptor {
29
+ options;
30
+ reporter;
31
+ constructor(options, reporter) {
32
+ this.options = options;
33
+ this.reporter = reporter;
34
+ }
35
+ intercept(context, next) {
36
+ if (!this.options.enabled || context.getType() !== 'http') {
37
+ return next.handle();
38
+ }
39
+ const http = context.switchToHttp();
40
+ const req = http.getRequest();
41
+ const res = http.getResponse();
42
+ const start = performance.now();
43
+ const traceId = readHeader(req, NEST_RN_LENS_HEADERS.traceId) ?? randomUUID();
44
+ // Lets the client, and the next service in the chain, reuse the trace id.
45
+ writeHeader(res, NEST_RN_LENS_HEADERS.traceId, traceId);
46
+ const finish = (status, error) => {
47
+ const event = {
48
+ id: randomUUID(),
49
+ traceId,
50
+ timestamp: Date.now(),
51
+ durationMs: Math.round(performance.now() - start),
52
+ source: {
53
+ app: readHeader(req, NEST_RN_LENS_HEADERS.app) ?? 'unknown',
54
+ caller: readHeader(req, NEST_RN_LENS_HEADERS.caller),
55
+ platform: detectPlatform(readHeader(req, 'user-agent')),
56
+ },
57
+ target: {
58
+ app: this.options.app,
59
+ method: req.method,
60
+ path: requestPath(req),
61
+ route: routePattern(req),
62
+ controller: context.getClass().name,
63
+ handler: context.getHandler().name,
64
+ },
65
+ status,
66
+ error,
67
+ };
68
+ this.reporter.report(event);
69
+ };
70
+ 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)),
73
+ }));
74
+ }
75
+ /**
76
+ * The status Nest is about to send. Depending on the platform it may not be
77
+ * written to the response yet, so follow Nest's own rules: @HttpCode(),
78
+ * otherwise 201 for POST and 200 for everything else.
79
+ */
80
+ successStatus(context, req, res) {
81
+ const declared = Reflect.getMetadata(HTTP_CODE_METADATA, context.getHandler());
82
+ if (declared) {
83
+ return declared;
84
+ }
85
+ // A handler using @Res() may have set its own status.
86
+ if (res.statusCode && res.statusCode !== 200) {
87
+ return res.statusCode;
88
+ }
89
+ return req.method === 'POST' ? 201 : 200;
90
+ }
91
+ };
92
+ NestRnLensInterceptor = __decorate([
93
+ Injectable(),
94
+ __param(0, Inject(NEST_RN_LENS_OPTIONS)),
95
+ __metadata("design:paramtypes", [Object, NestRnLensReporter])
96
+ ], NestRnLensInterceptor);
97
+ export { NestRnLensInterceptor };
@@ -0,0 +1,13 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import type { NestRnLensOptions } from './types.js';
3
+ export declare class NestRnLensModule {
4
+ /**
5
+ * Registers the interceptor for every controller in the app.
6
+ *
7
+ * ```ts
8
+ * @Module({ imports: [NestRnLensModule.forRoot({ app: 'api' })] })
9
+ * export class AppModule {}
10
+ * ```
11
+ */
12
+ static forRoot(options: NestRnLensOptions): DynamicModule;
13
+ }
@@ -0,0 +1,41 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var NestRnLensModule_1;
8
+ import { Module } from '@nestjs/common';
9
+ import { APP_INTERCEPTOR } from '@nestjs/core';
10
+ import { NEST_RN_LENS_OPTIONS } from './constants.js';
11
+ import { NestRnLensInterceptor } from './interceptor.js';
12
+ import { NestRnLensReporter } from './reporter.js';
13
+ let NestRnLensModule = NestRnLensModule_1 = class NestRnLensModule {
14
+ /**
15
+ * Registers the interceptor for every controller in the app.
16
+ *
17
+ * ```ts
18
+ * @Module({ imports: [NestRnLensModule.forRoot({ app: 'api' })] })
19
+ * export class AppModule {}
20
+ * ```
21
+ */
22
+ static forRoot(options) {
23
+ const resolved = {
24
+ ...options,
25
+ enabled: options.enabled ?? process.env.NODE_ENV !== 'production',
26
+ log: options.log ?? true,
27
+ };
28
+ return {
29
+ module: NestRnLensModule_1,
30
+ providers: [
31
+ { provide: NEST_RN_LENS_OPTIONS, useValue: resolved },
32
+ NestRnLensReporter,
33
+ { provide: APP_INTERCEPTOR, useClass: NestRnLensInterceptor },
34
+ ],
35
+ };
36
+ }
37
+ };
38
+ NestRnLensModule = NestRnLensModule_1 = __decorate([
39
+ Module({})
40
+ ], NestRnLensModule);
41
+ export { NestRnLensModule };
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,12 @@
1
+ import type { NestRnLensEvent, NestRnLensOptions } from './types.js';
2
+ /** Sends each event to the logger and to the `onEvent` callback. Never throws. */
3
+ export declare class NestRnLensReporter {
4
+ private readonly options;
5
+ private readonly logger;
6
+ private repoRoot?;
7
+ constructor(options: NestRnLensOptions);
8
+ report(event: NestRnLensEvent): void;
9
+ /** "mobile (ios) apps/mobile/src/screens/orders.tsx:17 → GET /orders → OrdersController.findAll 200 4ms" */
10
+ private describe;
11
+ private shortPath;
12
+ }
@@ -0,0 +1,75 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, Logger } from '@nestjs/common';
14
+ import { existsSync } from 'node:fs';
15
+ import { dirname, join, relative } from 'node:path';
16
+ import { LOGGER_CONTEXT, NEST_RN_LENS_OPTIONS } from './constants.js';
17
+ /** Sends each event to the logger and to the `onEvent` callback. Never throws. */
18
+ let NestRnLensReporter = class NestRnLensReporter {
19
+ options;
20
+ logger = new Logger(LOGGER_CONTEXT);
21
+ repoRoot;
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ report(event) {
26
+ try {
27
+ this.options.onEvent?.(event);
28
+ }
29
+ catch {
30
+ // A broken callback must not break the request it observes.
31
+ }
32
+ if (this.options.log === false) {
33
+ return;
34
+ }
35
+ try {
36
+ this.logger.log(this.describe(event));
37
+ this.logger.debug(JSON.stringify(event));
38
+ }
39
+ catch {
40
+ // Same for a broken logger.
41
+ }
42
+ }
43
+ /** "mobile (ios) apps/mobile/src/screens/orders.tsx:17 → GET /orders → OrdersController.findAll 200 4ms" */
44
+ describe({ source, target, status, durationMs }) {
45
+ const from = [`${source.app} (${source.platform})`, this.shortPath(source.caller)].filter(Boolean).join(' ');
46
+ return `${from} → ${target.method} ${target.route} → ${target.controller}.${target.handler} ${status} ${durationMs}ms`;
47
+ }
48
+ // Clients send absolute paths (editors need them to open the file); logs are
49
+ // easier to read relative to the monorepo root.
50
+ shortPath(file) {
51
+ if (!file?.startsWith('/')) {
52
+ return file;
53
+ }
54
+ this.repoRoot ??= findRepoRoot(process.cwd());
55
+ return relative(this.repoRoot, file);
56
+ }
57
+ };
58
+ NestRnLensReporter = __decorate([
59
+ Injectable(),
60
+ __param(0, Inject(NEST_RN_LENS_OPTIONS)),
61
+ __metadata("design:paramtypes", [Object])
62
+ ], NestRnLensReporter);
63
+ export { NestRnLensReporter };
64
+ const ROOT_MARKERS = ['turbo.json', 'pnpm-workspace.yaml', '.git'];
65
+ function findRepoRoot(from) {
66
+ let dir = from;
67
+ while (!ROOT_MARKERS.some((marker) => existsSync(join(dir, marker)))) {
68
+ const parent = dirname(dir);
69
+ if (parent === dir) {
70
+ return from;
71
+ }
72
+ dir = parent;
73
+ }
74
+ return dir;
75
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Headers a client sends so the interceptor knows who made the request.
3
+ * The React Native client (@nest-rn-lens/react-native) fills them in dev.
4
+ */
5
+ export declare const NEST_RN_LENS_HEADERS: {
6
+ /** Name of the calling app, e.g. "mobile". */
7
+ readonly app: "x-nest-rn-lens-app";
8
+ /** Where in the client the call was made, e.g. "/…/src/screens/OrderList.tsx:42". */
9
+ readonly caller: "x-nest-rn-lens-caller";
10
+ /** Same id across every hop of one request chain. Echoed back on the response. */
11
+ readonly traceId: "x-nest-rn-lens-trace-id";
12
+ };
13
+ export type Platform = 'ios' | 'android' | 'web' | 'unknown';
14
+ /** One request, as seen by the API. */
15
+ export interface NestRnLensEvent {
16
+ id: string;
17
+ traceId: string;
18
+ /** When the response was ready (ms since epoch). */
19
+ timestamp: number;
20
+ durationMs: number;
21
+ source: {
22
+ /** From the x-nest-rn-lens-app header, or "unknown". */
23
+ app: string;
24
+ /** From the x-nest-rn-lens-caller header. */
25
+ caller?: string;
26
+ /** Guessed from the User-Agent. */
27
+ platform: Platform;
28
+ };
29
+ target: {
30
+ /** The `app` option of this API. */
31
+ app: string;
32
+ method: string;
33
+ /** Actual URL path without the query string, e.g. "/orders/42". */
34
+ path: string;
35
+ /** Route pattern, e.g. "/orders/:id". Groups requests to the same endpoint. */
36
+ route: string;
37
+ controller: string;
38
+ handler: string;
39
+ };
40
+ status: number;
41
+ /** Message of the exception, when the handler threw. */
42
+ error?: string;
43
+ }
44
+ export interface NestRnLensOptions {
45
+ /** Name of this API in the traffic graph, e.g. "api". */
46
+ app: string;
47
+ /**
48
+ * Turns reporting on or off. Defaults to `true` unless NODE_ENV is
49
+ * "production", so nothing runs in production unless you opt in.
50
+ */
51
+ enabled?: boolean;
52
+ /**
53
+ * Called with every event. Use it to forward traffic somewhere else.
54
+ * Errors thrown here are ignored, so they never affect the request.
55
+ */
56
+ onEvent?: (event: NestRnLensEvent) => void;
57
+ /**
58
+ * Log every event through Nest's Logger (a readable line, plus the JSON at
59
+ * debug level that the NestRN Lens VS Code extension reads). Defaults to `true`.
60
+ */
61
+ log?: boolean;
62
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Headers a client sends so the interceptor knows who made the request.
3
+ * The React Native client (@nest-rn-lens/react-native) fills them in dev.
4
+ */
5
+ export const NEST_RN_LENS_HEADERS = {
6
+ /** Name of the calling app, e.g. "mobile". */
7
+ app: 'x-nest-rn-lens-app',
8
+ /** Where in the client the call was made, e.g. "/…/src/screens/OrderList.tsx:42". */
9
+ caller: 'x-nest-rn-lens-caller',
10
+ /** Same id across every hop of one request chain. Echoed back on the response. */
11
+ traceId: 'x-nest-rn-lens-trace-id',
12
+ };
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@nest-rn-lens/nest",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "NestJS interceptor that reports every request together with the React Native screen that made it. Part of NestRN Lens.",
5
+ "keywords": [
6
+ "nestjs",
7
+ "nest",
8
+ "interceptor",
9
+ "react-native",
10
+ "expo",
11
+ "turborepo",
12
+ "monorepo",
13
+ "observability",
14
+ "tracing",
15
+ "devtools"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "isa",
19
+ "main": "./dist/cjs/index.js",
20
+ "types": "./dist/cjs/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/esm/index.d.ts",
25
+ "default": "./dist/esm/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/cjs/index.d.ts",
29
+ "default": "./dist/cjs/index.js"
30
+ }
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "sideEffects": false,
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "scripts": {
44
+ "clean": "node -e \"for (const d of ['dist', 'dist-test']) require('fs').rmSync(d, { recursive: true, force: true })\"",
45
+ "build": "npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.esm.json && node scripts/mark-formats.cjs",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "npm run clean && tsc -p tsconfig.test.json && node --test \"dist-test/test/**/*.test.js\"",
48
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
49
+ },
50
+ "peerDependencies": {
51
+ "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
52
+ "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
53
+ "rxjs": "^7.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "devDependencies": {
59
+ "@nestjs/common": "^12.1.2",
60
+ "@nestjs/core": "^12.1.2",
61
+ "@nestjs/platform-express": "^12.1.2",
62
+ "@nestjs/platform-fastify": "^12.1.2",
63
+ "@types/node": "^22.20.5",
64
+ "reflect-metadata": "^0.2.2",
65
+ "rxjs": "^7.8.2",
66
+ "typescript": "^6.0.3"
67
+ },
68
+ "module": "./dist/esm/index.js"
69
+ }