Skip to content

Commit 9b2ad6f

Browse files
Merge pull request #6 from rakibulislam8226/pre-master
Refactor withTimeout function for improved clarity and efficiency; en…
2 parents 58a521e + 3ccbead commit 9b2ad6f

29 files changed

Lines changed: 2264 additions & 224 deletions

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
## [Unreleased]
1515

1616
### Added
17+
- **`NEST_DEBUG_PANEL_ENABLED` environment variable.** Toggle the panel on or off
18+
straight from the environment — no code change. When set to a recognized
19+
boolean (`true`/`1`/`yes`/`on` or `false`/`0`/`no`/`off`, case-insensitive) it
20+
**takes precedence** over both the `enabled` option and the `NODE_ENV` default,
21+
so you can force it on in any environment or disable it in development. Leaving
22+
it unset preserves the existing behavior exactly.
23+
- **Redesigned dashboard.** The UI is now a Telescope-style single-page app: a
24+
fixed left sidebar of **monitors** (Overview, Requests, Sockets, Queries,
25+
Logs, Exceptions, Slow) with live counts, and an **Overview** landing page
26+
with KPI tiles (total requests, average latency, error rate, slow count, total
27+
SQL, N+1 alerts), a latency chart and a recent-activity feed. Adds a global
28+
**Queries** view (every SQL query across all requests, with N+1/duplicate
29+
flags), plus **Exceptions** and **Slow** views, a filter/search box, relative
30+
timestamps and a live-connection indicator. Fully responsive down to mobile,
31+
and auto-refresh only re-renders when new data is captured (no flicker).
32+
- **Log capture.** `console.*` output emitted while a request or socket event is
33+
executing is attached to that request's profile and surfaced in the new
34+
**Logs** monitor (and a per-request Logs tab), with level, message and logger
35+
context. The console is patched at bootstrap and restored on shutdown;
36+
original output still prints. Turn it off with `captureLogs: false`.
37+
- **Automatic socket.io event capture.** Inbound NestJS WebSocket handlers
38+
(`@SubscribeMessage`) are now captured like HTTP requests with **zero extra
39+
setup**`DebugModule.forRoot()` is all you need, no per-gateway decorator.
40+
(NestJS does not apply global interceptors to gateways, so the panel attaches
41+
itself to every gateway at startup.) Each handler runs inside the same tracing
42+
context, so every SQL/Redis/HTTP call it makes is recorded automatically, with
43+
N+1 detection and a timeline, plus the event name, namespace, socket id, rooms,
44+
handshake (redacted), payload and acknowledgement. Socket events appear in the
45+
**same list** as HTTP requests with a `WS` badge and an All / HTTP / Socket
46+
filter. Turn it off with `sockets: false`. An optional `@TrackSocketEvents()`
47+
decorator is exported for edge cases the auto-attach can't reach. No new
48+
dependency is added, and HTTP capture is unchanged.
1749
- **Prisma 7 zero-config raw SQL capture.** Auto-instrumentation now wraps the
1850
Prisma driver adapter (e.g. `@prisma/adapter-pg`) directly, so the actual SQL
1951
text, params and timing are captured without setting the `log` option. Each

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Start your app and open:
5454
http://localhost:<your-port>/__debug
5555
```
5656

57-
That's it. Hit any endpoint of your API and watch it appear in the dashboard (it refreshes every 2 seconds). If the list stays empty, make sure `NODE_ENV` isn't `production`, or pass `enabled: true` explicitly.
57+
That's it. Hit any endpoint of your API and watch it appear in the dashboard (it refreshes every 2 seconds). If the list stays empty, make sure `NODE_ENV` isn't `production`, pass `enabled: true` explicitly, or set `NEST_DEBUG_PANEL_ENABLED=true` in your environment (see [Enabling & disabling](#enabling--disabling)).
5858

5959
Works with Node.js 18+, NestJS 9/10/11, and both Express and Fastify. No runtime dependencies.
6060

@@ -70,6 +70,17 @@ On top of that:
7070
- **Exceptions** with name, message, stack trace, and how long the request ran before failing
7171
- **Memory**: heap and RSS deltas per request, event-loop delay
7272
- **A timeline** that lays all of the above in order, plus your own custom marks
73+
- **Socket.io events** — inbound `@SubscribeMessage` handlers captured like a request, with all the SQL/Redis/HTTP they run (see below)
74+
75+
## Socket.io events
76+
77+
If your app uses NestJS WebSocket gateways (`@WebSocketGateway` + `@SubscribeMessage`), every incoming event is captured **automatically** — just like HTTP. You don't touch your gateways or add any decorator; `DebugModule.forRoot()` is all it takes. (NestJS doesn't apply global interceptors to gateways, so the panel attaches itself to each gateway at startup for you.)
78+
79+
Each event runs inside the same tracing context, so **every query it runs shows up automatically**, with N+1 detection, timeline and all — plus the event name, namespace, socket id, rooms, handshake (redacted), payload and acknowledgement.
80+
81+
Socket events appear in the **same list** as HTTP requests, tagged with a `WS` badge; use the **All / HTTP / Socket** filter at the top to narrow down. Set `sockets: false` in `forRoot()` to turn socket capture off.
82+
83+
> Capture is on by default. For rare cases the automatic attachment can't reach (e.g. a single handler, or a gateway created outside the module scan), the `@TrackSocketEvents()` decorator is exported as an explicit opt-in — normally you won't need it.
7384
7485
## Database, Redis and HTTP capture is automatic
7586

@@ -94,7 +105,7 @@ Everything is optional. These are the defaults:
94105

95106
```ts
96107
DebugModule.forRoot({
97-
enabled: process.env.NODE_ENV !== 'production',
108+
enabled: process.env.NODE_ENV !== 'production', // NEST_DEBUG_PANEL_ENABLED env var overrides this
98109
maxRequests: 200, // how many profiles to keep; oldest are evicted
99110
captureRequestBody: true,
100111
captureResponseBody: true,
@@ -103,6 +114,8 @@ DebugModule.forRoot({
103114
captureSql: true,
104115
captureRedis: true,
105116
captureHttp: true,
117+
captureLogs: true, // capture console.* emitted during a request (Logs monitor)
118+
sockets: true, // capture socket.io gateway events (automatic, no per-gateway setup)
106119
autoInstrument: true, // scan providers and hook them automatically
107120
slowQueryThreshold: 100, // ms; queries at or above get flagged
108121
slowRequestThreshold: 500, // ms; requests at or above get flagged
@@ -121,6 +134,22 @@ DebugModule.forRoot({
121134

122135
`forRootAsync({ imports, useFactory, inject, routePrefix })` works too. Note that `routePrefix` must be static in async mode, because routes are registered before async factories run.
123136

137+
### Enabling & disabling
138+
139+
The panel resolves its on/off state with this precedence (first match wins):
140+
141+
1. **`NEST_DEBUG_PANEL_ENABLED` environment variable** — when set to a recognized boolean, it overrides everything below, so you can flip the panel on or off **without changing code**.
142+
2. **The `enabled` option** passed to `forRoot()` / `forRootAsync()`.
143+
3. **Default** — on when `NODE_ENV !== 'production'`, off otherwise.
144+
145+
```bash
146+
NEST_DEBUG_PANEL_ENABLED=true # force ON anywhere — even in production
147+
NEST_DEBUG_PANEL_ENABLED=false # force OFF anywhere — even in development
148+
# (unset) # fall back to the `enabled` option, then NODE_ENV
149+
```
150+
151+
Accepted values are case-insensitive: `true` / `1` / `yes` / `on` enable it, `false` / `0` / `no` / `off` disable it. Anything unrecognized (or an unset/empty var) is ignored, so the option and `NODE_ENV` default still apply. When disabled, the interceptor passes every request straight through, nothing is instrumented or stored, and the dashboard routes return 404.
152+
124153
To exclude routes from profiling, use the `ignore` option (`'/health'`, globs like `'/static/*'`, or RegExps) or put `@DebugIgnore()` on a controller or handler. The panel's own routes are always excluded.
125154

126155
## Works with any ORM, any database
@@ -258,7 +287,7 @@ Build your own frontend or tooling on top of it if you like.
258287

259288
## Security
260289

261-
- Off in production automatically, unless you explicitly set `enabled: true`. When off, the interceptor passes requests straight through and the debug routes return 404.
290+
- Off in production automatically, unless you set `enabled: true` or `NEST_DEBUG_PANEL_ENABLED=true` (see [Enabling & disabling](#enabling--disabling)). When off, the interceptor passes requests straight through and the debug routes return 404.
262291
- Sensitive body keys and headers are redacted before anything is stored.
263292
- Gate the dashboard with `authorize: (req) => req.user?.isAdmin === true`.
264293
- Profiles live in process memory by default and never leave your machine.

example/app.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Module } from '@nestjs/common';
22
import { DebugModule, FetchPlugin } from '../src';
33
import { DemoController } from './demo.controller';
4+
import { DemoGateway } from './demo.gateway';
45
import { FakeDatabaseService } from './fake-database.service';
56

67
@Module({
@@ -23,6 +24,6 @@ import { FakeDatabaseService } from './fake-database.service';
2324
}),
2425
],
2526
controllers: [DemoController],
26-
providers: [FakeDatabaseService],
27+
providers: [FakeDatabaseService, DemoGateway],
2728
})
2829
export class AppModule {}

example/demo.gateway.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
ConnectedSocket,
3+
MessageBody,
4+
SubscribeMessage,
5+
WebSocketGateway,
6+
} from '@nestjs/websockets';
7+
import type { Socket } from 'socket.io';
8+
import { FakeDatabaseService } from './fake-database.service';
9+
10+
/**
11+
* Socket.io gateway for the demo. No debug-panel setup here at all — every
12+
* @SubscribeMessage handler is captured automatically (just like HTTP), and the
13+
* SQL each one runs shows up in the panel with N+1 detection.
14+
*/
15+
@WebSocketGateway({ cors: { origin: '*' } })
16+
export class DemoGateway {
17+
constructor(private readonly db: FakeDatabaseService) {}
18+
19+
@SubscribeMessage('users.list')
20+
async listUsers(): Promise<{ users: Array<{ id: number; name: string }> }> {
21+
const users = await this.db.findUsers();
22+
return { users };
23+
}
24+
25+
@SubscribeMessage('users.get')
26+
async getUser(@MessageBody() payload: { id: number }): Promise<{ user: unknown }> {
27+
const user = await this.db.findUser(Number(payload?.id ?? 1));
28+
return { user };
29+
}
30+
31+
// Triggers the N+1 detector — one query per user, captured on the Sockets page.
32+
@SubscribeMessage('users.withPosts')
33+
async withPosts(): Promise<{ users: unknown[] }> {
34+
const users = await this.db.findUsersWithPosts();
35+
return { users };
36+
}
37+
38+
@SubscribeMessage('chat.send')
39+
async send(
40+
@MessageBody() payload: { room?: string; text: string },
41+
@ConnectedSocket() client: Socket,
42+
): Promise<{ delivered: true }> {
43+
await this.db.findUser(1); // pretend we persist the message
44+
if (payload?.room) client.to(payload.room).emit('chat.new', payload);
45+
return { delivered: true };
46+
}
47+
48+
@SubscribeMessage('boom')
49+
fail(): never {
50+
throw new Error('socket handler blew up on purpose');
51+
}
52+
}

example/main.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,52 @@
11
import { NestFactory } from '@nestjs/core';
2+
import { IoAdapter } from '@nestjs/platform-socket.io';
3+
import { io } from 'socket.io-client';
24
import { AppModule } from './app.module';
35

6+
/**
7+
* Fires a few socket events at our own server so the panel has socket data to
8+
* show as soon as you open it — no separate command needed. Dev-demo only.
9+
*/
10+
async function driveSocketDemo(port: number): Promise<void> {
11+
const socket = io(`http://localhost:${port}`, { transports: ['websocket'] });
12+
const emit = (event: string, payload?: unknown): Promise<unknown> =>
13+
new Promise((resolve) => {
14+
const timer = setTimeout(() => resolve('(no ack)'), 1000);
15+
socket.emit(event, payload, (ack: unknown) => {
16+
clearTimeout(timer);
17+
resolve(ack);
18+
});
19+
});
20+
21+
await new Promise<void>((resolve) => socket.on('connect', () => resolve()));
22+
await emit('users.list');
23+
await emit('users.get', { id: 2 });
24+
await emit('users.withPosts'); // N+1 demo
25+
await emit('chat.send', { text: 'hello', password: 's3cret' });
26+
socket.disconnect();
27+
}
28+
429
async function bootstrap(): Promise<void> {
530
const app = await NestFactory.create(AppModule);
31+
app.useWebSocketAdapter(new IoAdapter(app)); // enable socket.io
632
const port = Number(process.env.PORT ?? 3000);
733
await app.listen(port);
34+
35+
// Populate some socket events automatically (dev demo).
36+
void driveSocketDemo(port).catch(() => undefined);
37+
838
console.log(`
939
Example app: http://localhost:${port}
10-
Debug dashboard: http://localhost:${port}/__debug
40+
Debug panel: http://localhost:${port}/__debug (HTTP + Socket, filter at the top)
1141
12-
Try:
42+
Try more HTTP:
1343
curl http://localhost:${port}/users
14-
curl http://localhost:${port}/users/1
15-
curl -X POST http://localhost:${port}/users -H 'content-type: application/json' -d '{"name":"Joan","password":"s3cret"}'
1644
curl http://localhost:${port}/n-plus-one # N+1 detection demo
17-
curl http://localhost:${port}/slow # slow query + slow request
18-
curl http://localhost:${port}/external # outgoing HTTP capture
1945
curl http://localhost:${port}/boom # exception capture
20-
`);
46+
47+
Socket events are fired automatically at startup and show up in the panel
48+
under the "Socket" filter — no extra command, no gateway setup needed.
49+
`);
2150
}
2251

2352
void bootstrap();

0 commit comments

Comments
 (0)