@ngn-net/nestjs-telescope 0.1.12 → 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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @ngn-net/nestjs-telescope
2
2
 
3
+ ![NestJS Telescope Banner](assets/hero.png)
4
+
3
5
  A full-featured developer assistant and application monitoring suite for NestJS, inspired by Laravel Telescope. It monitors incoming HTTP requests, database queries, cache operations, background queue jobs, application events, outgoing mail, logs, unhandled exceptions, cron job schedules, and Redis commands.
4
6
 
5
7
  All telemetry records are persisted to a database table and accessible via a modern, high-fidelity dark-themed single-page application dashboard.
@@ -93,6 +95,33 @@ import { TelescopeModule, EntryType } from '@ngn-net/nestjs-telescope';
93
95
  export class AppModule {}
94
96
  ```
95
97
 
98
+ ### Async Configuration
99
+
100
+ You can also register `TelescopeModule` asynchronously using a factory (e.g. inject `ConfigService`):
101
+
102
+ ```typescript
103
+ import { Module } from '@nestjs/common';
104
+ import { ConfigModule, ConfigService } from '@nestjs/config';
105
+ import { TelescopeModule } from '@ngn-net/nestjs-telescope';
106
+
107
+ @Module({
108
+ imports: [
109
+ ConfigModule.forRoot(),
110
+ TelescopeModule.forRootAsync({
111
+ imports: [ConfigModule],
112
+ inject: [ConfigService],
113
+ useFactory: (configService: ConfigService) => ({
114
+ path: configService.get('TELESCOPE_PATH', 'telescope'),
115
+ password: configService.get('TELESCOPE_PASSWORD', 'password'),
116
+ jwtSecret: configService.get('TELESCOPE_JWT_SECRET', 'my-secret'),
117
+ maxEntries: 1000,
118
+ }),
119
+ }),
120
+ ],
121
+ })
122
+ export class AppModule {}
123
+ ```
124
+
96
125
  ---
97
126
 
98
127
  ## Dashboard Access
@@ -0,0 +1,5 @@
1
+ import { NestMiddleware } from '@nestjs/common';
2
+ import { Request, Response, NextFunction } from 'express';
3
+ export declare class DashboardStaticMiddleware implements NestMiddleware {
4
+ use(req: Request, res: Response, next: NextFunction): void;
5
+ }
@@ -0,0 +1,23 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.DashboardStaticMiddleware = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ let DashboardStaticMiddleware = class DashboardStaticMiddleware {
12
+ use(req, res, next) {
13
+ // Let Nest's ServeStatic handle static files; fallback to index.html for SPA routing
14
+ if (req.baseUrl && req.path.startsWith(req.baseUrl)) {
15
+ return next();
16
+ }
17
+ next();
18
+ }
19
+ };
20
+ exports.DashboardStaticMiddleware = DashboardStaticMiddleware;
21
+ exports.DashboardStaticMiddleware = DashboardStaticMiddleware = __decorate([
22
+ (0, common_1.Injectable)()
23
+ ], DashboardStaticMiddleware);
@@ -1,5 +1,6 @@
1
1
  export declare enum EntryType {
2
2
  REQUEST = "request",
3
+ HTTP_CLIENT = "http_client",
3
4
  QUERY = "query",
4
5
  CACHE = "cache",
5
6
  JOB = "job",
@@ -5,6 +5,7 @@ exports.EntryType = void 0;
5
5
  var EntryType;
6
6
  (function (EntryType) {
7
7
  EntryType["REQUEST"] = "request";
8
+ EntryType["HTTP_CLIENT"] = "http_client";
8
9
  EntryType["QUERY"] = "query";
9
10
  EntryType["CACHE"] = "cache";
10
11
  EntryType["JOB"] = "job";
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './interfaces/telescope-options.interface';
7
7
  export * from './interfaces/entry.interface';
8
8
  export * from './guards/telescope-jwt.guard';
9
9
  export * from './constants';
10
+ export * from './dashboard.provider';
10
11
  export * from './watchers/http-request.watcher';
11
12
  export * from './watchers/query.watcher';
12
13
  export * from './watchers/cache.watcher';
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __exportStar(require("./interfaces/telescope-options.interface"), exports);
24
24
  __exportStar(require("./interfaces/entry.interface"), exports);
25
25
  __exportStar(require("./guards/telescope-jwt.guard"), exports);
26
26
  __exportStar(require("./constants"), exports);
27
+ __exportStar(require("./dashboard.provider"), exports);
27
28
  // Export all watchers for custom usage/extension
28
29
  __exportStar(require("./watchers/http-request.watcher"), exports);
29
30
  __exportStar(require("./watchers/query.watcher"), exports);
@@ -4,6 +4,8 @@ export interface TelescopeOptions {
4
4
  path?: string;
5
5
  /** JWT secret used for UI authentication */
6
6
  jwtSecret?: string;
7
+ /** Enable the dashboard UI (default: true) */
8
+ enableDashboard?: boolean;
7
9
  /** Password for dashboard login (default: 'password', or env TELESCOPE_PASSWORD) */
8
10
  password?: string;
9
11
  /** Which entry types to enable (default: all) */
@@ -17,3 +19,13 @@ export interface TelescopeOptions {
17
19
  /** Additional middleware to apply to UI routes */
18
20
  uiMiddleware?: any[];
19
21
  }
22
+ export interface TelescopeOptionsFactory {
23
+ createTelescopeOptions(): Promise<TelescopeOptions> | TelescopeOptions;
24
+ }
25
+ export interface TelescopeModuleAsyncOptions {
26
+ imports?: any[];
27
+ useExisting?: any;
28
+ useClass?: any;
29
+ useFactory?: (...args: any[]) => Promise<TelescopeOptions> | TelescopeOptions;
30
+ inject?: any[];
31
+ }
@@ -1,10 +1,14 @@
1
1
  import { DynamicModule, OnModuleInit } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
+ import { TelescopeOptions, TelescopeModuleAsyncOptions } from './interfaces/telescope-options.interface';
3
4
  export declare class TelescopeModule implements OnModuleInit {
4
5
  private readonly moduleRef;
5
- constructor(moduleRef: ModuleRef);
6
+ private readonly options;
7
+ constructor(moduleRef: ModuleRef, options: TelescopeOptions);
6
8
  onModuleInit(): void;
7
- static forRoot(options?: any): DynamicModule;
8
- private static registerWatchers;
9
- static forRootAsync(optionsFactory: any): DynamicModule;
9
+ static forRoot(options?: TelescopeOptions): DynamicModule;
10
+ static forRootAsync(options: TelescopeModuleAsyncOptions): DynamicModule;
11
+ private static createAsyncProviders;
12
+ private static registerWatchersAndModules;
13
+ private static registerWatchersAndModulesAsync;
10
14
  }
@@ -8,10 +8,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  var TelescopeModule_1;
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.TelescopeModule = void 0;
14
- // src/telescope.module.ts
15
17
  const common_1 = require("@nestjs/common");
16
18
  const serve_static_1 = require("@nestjs/serve-static");
17
19
  const path_1 = require("path");
@@ -37,10 +39,38 @@ const constants_1 = require("./constants");
37
39
  const entry_type_enum_1 = require("./enums/entry-type.enum");
38
40
  let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
39
41
  moduleRef;
40
- constructor(moduleRef) {
42
+ options;
43
+ constructor(moduleRef, options) {
41
44
  this.moduleRef = moduleRef;
45
+ this.options = options;
46
+ const path = this.options.path || constants_1.DEFAULT_TELESCOPE_PATH;
47
+ Reflect.defineMetadata('path', path, telescope_controller_1.TelescopeController);
42
48
  }
43
49
  onModuleInit() {
50
+ // Dynamic peer dependency validation at bootstrap
51
+ const hasDependency = (type, packageName) => {
52
+ try {
53
+ require(packageName);
54
+ return true;
55
+ }
56
+ catch (e) {
57
+ if (this.options.enabledEntryTypes && this.options.enabledEntryTypes.includes(type)) {
58
+ throw new Error(`NestJS Telescope Error: EntryType '${type}' was explicitly enabled in config, but the required peer dependency '${packageName}' is not installed.`);
59
+ }
60
+ return false;
61
+ }
62
+ };
63
+ hasDependency(entry_type_enum_1.EntryType.CACHE, '@nestjs/cache-manager');
64
+ const hasBull = hasDependency(entry_type_enum_1.EntryType.JOB, '@nestjs/bullmq');
65
+ const hasBullmq = hasDependency(entry_type_enum_1.EntryType.JOB, 'bullmq');
66
+ if (this.options.enabledEntryTypes && this.options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.JOB)) {
67
+ if (!hasBull || !hasBullmq) {
68
+ throw new Error(`NestJS Telescope Error: EntryType 'JOB' was explicitly enabled in config, but the required peer dependencies '@nestjs/bullmq' and 'bullmq' are not installed.`);
69
+ }
70
+ }
71
+ hasDependency(entry_type_enum_1.EntryType.EVENT, '@nestjs/event-emitter');
72
+ hasDependency(entry_type_enum_1.EntryType.SCHEDULED_TASK, '@nestjs/schedule');
73
+ hasDependency(entry_type_enum_1.EntryType.MAIL, 'nodemailer');
44
74
  // Dynamic EventEmitter2 configuration
45
75
  try {
46
76
  const eventWatcher = this.moduleRef.get(event_watcher_1.EventWatcher, { strict: false });
@@ -81,7 +111,7 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
81
111
  // Serve static UI assets if dashboard is enabled (default true)
82
112
  if (options.enableDashboard !== false) {
83
113
  const staticPath = (0, path_1.join)(__dirname, '..', 'ui');
84
- const routePath = options.path || 'telescope';
114
+ const routePath = options.path || constants_1.DEFAULT_TELESCOPE_PATH;
85
115
  imports.push(serve_static_1.ServeStaticModule.forRoot({
86
116
  rootPath: staticPath,
87
117
  serveRoot: `/${routePath}`,
@@ -96,145 +126,209 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
96
126
  telescope_repository_service_1.TelescopeRepository,
97
127
  telescope_jwt_guard_1.JwtAuthGuard,
98
128
  ];
99
- // Always registered watchers (no external peer dependencies needed)
100
- const watchersToRegister = [
129
+ // Register watchers (both core and optional)
130
+ this.registerWatchersAndModules(options, providers, imports);
131
+ return {
132
+ module: TelescopeModule_1,
133
+ imports,
134
+ providers,
135
+ controllers: [telescope_controller_1.TelescopeController],
136
+ exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
137
+ };
138
+ }
139
+ static forRootAsync(options) {
140
+ const providers = [
141
+ ...this.createAsyncProviders(options),
142
+ telescope_service_1.TelescopeService,
143
+ telescope_repository_service_1.TelescopeRepository,
144
+ telescope_jwt_guard_1.JwtAuthGuard,
145
+ ];
146
+ const imports = [
147
+ typeorm_1.TypeOrmModule.forFeature([telescope_entry_entity_1.TelescopeEntry]),
148
+ jwt_1.JwtModule.registerAsync({
149
+ imports: options.imports || [],
150
+ inject: [constants_1.TELESCOPE_OPTIONS],
151
+ useFactory: async (telescopeOpts) => ({
152
+ secret: telescopeOpts.jwtSecret || process.env.TELESCOPE_JWT_SECRET || constants_1.DEFAULT_JWT_SECRET,
153
+ signOptions: { expiresIn: '1d' },
154
+ }),
155
+ }),
156
+ ];
157
+ // Serve static UI assets if dashboard is enabled (default true)
158
+ imports.push(serve_static_1.ServeStaticModule.forRootAsync({
159
+ imports: options.imports || [],
160
+ inject: [constants_1.TELESCOPE_OPTIONS],
161
+ useFactory: (telescopeOpts) => {
162
+ if (telescopeOpts.enableDashboard === false) {
163
+ return [];
164
+ }
165
+ const staticPath = (0, path_1.join)(__dirname, '..', 'ui');
166
+ const routePath = telescopeOpts.path || constants_1.DEFAULT_TELESCOPE_PATH;
167
+ return [
168
+ {
169
+ rootPath: staticPath,
170
+ serveRoot: `/${routePath}`,
171
+ },
172
+ ];
173
+ },
174
+ }));
175
+ // Register all available/supported watchers & modules dynamically
176
+ this.registerWatchersAndModulesAsync(providers, imports);
177
+ return {
178
+ module: TelescopeModule_1,
179
+ imports,
180
+ providers,
181
+ controllers: [telescope_controller_1.TelescopeController],
182
+ exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
183
+ };
184
+ }
185
+ static createAsyncProviders(options) {
186
+ if (options.useFactory) {
187
+ return [
188
+ {
189
+ provide: constants_1.TELESCOPE_OPTIONS,
190
+ useFactory: options.useFactory,
191
+ inject: options.inject || [],
192
+ },
193
+ ];
194
+ }
195
+ const useClass = options.useClass || options.useExisting;
196
+ if (useClass) {
197
+ return [
198
+ {
199
+ provide: constants_1.TELESCOPE_OPTIONS,
200
+ useFactory: async (optionsFactory) => await optionsFactory.createTelescopeOptions(),
201
+ inject: [useClass],
202
+ },
203
+ {
204
+ provide: useClass,
205
+ useClass: options.useClass,
206
+ },
207
+ ];
208
+ }
209
+ return [];
210
+ }
211
+ static registerWatchersAndModules(options, providers, imports) {
212
+ // 1. Core watchers (always registered if enabled or not specified)
213
+ const coreWatchers = [
101
214
  { type: entry_type_enum_1.EntryType.REQUEST, watcher: http_request_watcher_1.HttpRequestWatcher, isInterceptor: true },
102
215
  { type: entry_type_enum_1.EntryType.QUERY, watcher: query_watcher_1.QueryWatcher },
103
216
  { type: entry_type_enum_1.EntryType.LOG, watcher: log_watcher_1.LogWatcher },
104
217
  { type: entry_type_enum_1.EntryType.EXCEPTION, watcher: exception_watcher_1.ExceptionWatcher, isInterceptor: true },
105
218
  { type: entry_type_enum_1.EntryType.REDIS, watcher: redis_watcher_1.RedisWatcher },
106
219
  ];
107
- // Register core watchers
108
- this.registerWatchers(options, providers);
109
- // Optional dependencies checking:
110
- // 1. CacheWatcher (@nestjs/cache-manager)
111
- let hasCache = false;
220
+ for (const item of coreWatchers) {
221
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(item.type)) {
222
+ if (item.isInterceptor) {
223
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: item.watcher });
224
+ }
225
+ else {
226
+ providers.push(item.watcher);
227
+ }
228
+ }
229
+ }
230
+ // 2. CacheWatcher
112
231
  try {
113
232
  require('@nestjs/cache-manager');
114
- hasCache = true;
115
- }
116
- catch { }
117
- if (hasCache && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.CACHE))) {
118
- try {
233
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.CACHE)) {
119
234
  const { CacheModule } = require('@nestjs/cache-manager');
120
- if (CacheModule) {
121
- imports.push(CacheModule.register({}));
122
- providers.push(cache_watcher_1.CacheWatcher);
123
- }
235
+ imports.push(CacheModule.register({}));
236
+ providers.push(cache_watcher_1.CacheWatcher);
124
237
  }
125
- catch { }
126
238
  }
127
- // 2. QueueWatcher (@nestjs/bullmq & bullmq)
128
- let hasBull = false;
239
+ catch (e) { }
240
+ // 3. QueueWatcher
129
241
  try {
130
242
  require('@nestjs/bullmq');
131
243
  require('bullmq');
132
- hasBull = true;
133
- }
134
- catch { }
135
- if (hasBull && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.JOB))) {
136
- try {
244
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.JOB)) {
137
245
  const { BullModule } = require('@nestjs/bullmq');
138
- if (BullModule) {
139
- imports.push(BullModule.registerQueue({
140
- name: 'telescope-queue',
141
- connection: {
142
- host: process.env.REDIS_HOST || '127.0.0.1',
143
- port: Number(process.env.REDIS_PORT) || 6379,
144
- },
145
- }));
146
- providers.push(queue_watcher_1.QueueWatcher);
147
- }
246
+ imports.push(BullModule.registerQueue({
247
+ name: 'telescope-queue',
248
+ connection: {
249
+ host: process.env.REDIS_HOST || '127.0.0.1',
250
+ port: Number(process.env.REDIS_PORT) || 6379,
251
+ },
252
+ }));
253
+ providers.push(queue_watcher_1.QueueWatcher);
148
254
  }
149
- catch { }
150
255
  }
151
- // 3. EventWatcher (@nestjs/event-emitter)
152
- let hasEvents = false;
256
+ catch (e) { }
257
+ // 4. EventWatcher
153
258
  try {
154
259
  require('@nestjs/event-emitter');
155
- hasEvents = true;
260
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.EVENT)) {
261
+ providers.push(event_watcher_1.EventWatcher);
262
+ }
263
+ }
264
+ catch (e) { }
265
+ // 5. ScheduleWatcher
266
+ try {
267
+ require('@nestjs/schedule');
268
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.SCHEDULED_TASK)) {
269
+ providers.push(schedule_watcher_1.ScheduleWatcher);
270
+ }
271
+ }
272
+ catch (e) { }
273
+ // 6. MailWatcher
274
+ try {
275
+ require('nodemailer');
276
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.MAIL)) {
277
+ providers.push(mail_watcher_1.MailWatcher);
278
+ }
279
+ }
280
+ catch (e) { }
281
+ }
282
+ static registerWatchersAndModulesAsync(providers, imports) {
283
+ // Core interceptors and watchers
284
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: http_request_watcher_1.HttpRequestWatcher });
285
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: exception_watcher_1.ExceptionWatcher });
286
+ providers.push(query_watcher_1.QueryWatcher);
287
+ providers.push(log_watcher_1.LogWatcher);
288
+ providers.push(redis_watcher_1.RedisWatcher);
289
+ // Optional modules/watchers if packages are present
290
+ try {
291
+ require('@nestjs/cache-manager');
292
+ const { CacheModule } = require('@nestjs/cache-manager');
293
+ imports.push(CacheModule.register({}));
294
+ providers.push(cache_watcher_1.CacheWatcher);
156
295
  }
157
- catch { }
158
- if (hasEvents && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.EVENT))) {
296
+ catch (e) { }
297
+ try {
298
+ require('@nestjs/bullmq');
299
+ require('bullmq');
300
+ const { BullModule } = require('@nestjs/bullmq');
301
+ imports.push(BullModule.registerQueue({
302
+ name: 'telescope-queue',
303
+ connection: {
304
+ host: process.env.REDIS_HOST || '127.0.0.1',
305
+ port: Number(process.env.REDIS_PORT) || 6379,
306
+ },
307
+ }));
308
+ providers.push(queue_watcher_1.QueueWatcher);
309
+ }
310
+ catch (e) { }
311
+ try {
312
+ require('@nestjs/event-emitter');
159
313
  providers.push(event_watcher_1.EventWatcher);
160
314
  }
161
- // 4. ScheduleWatcher (@nestjs/schedule)
162
- let hasSchedule = false;
315
+ catch (e) { }
163
316
  try {
164
317
  require('@nestjs/schedule');
165
- hasSchedule = true;
166
- }
167
- catch { }
168
- if (hasSchedule && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.SCHEDULED_TASK))) {
169
318
  providers.push(schedule_watcher_1.ScheduleWatcher);
170
319
  }
171
- // 5. MailWatcher (nodemailer)
172
- let hasNodemailer = false;
320
+ catch (e) { }
173
321
  try {
174
322
  require('nodemailer');
175
- hasNodemailer = true;
176
- }
177
- catch { }
178
- if (hasNodemailer && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.MAIL))) {
179
323
  providers.push(mail_watcher_1.MailWatcher);
180
324
  }
181
- // Dynamic Route Prefix Setup using Reflect metadata on TelescopeController
182
- const path = options.path || 'telescope';
183
- Reflect.defineMetadata('path', path, telescope_controller_1.TelescopeController);
184
- // Ensure static assets are served under the same path when dashboard enabled
185
- if (options.enableDashboard !== false) {
186
- // ServeStaticModule already handles static files; no extra code needed here.
187
- }
188
- return {
189
- module: TelescopeModule_1,
190
- imports,
191
- providers,
192
- controllers: [telescope_controller_1.TelescopeController],
193
- exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
194
- };
195
- }
196
- // Helper to register core watchers
197
- static registerWatchers(options, providers) {
198
- const watchersToRegister = [
199
- { type: entry_type_enum_1.EntryType.REQUEST, watcher: http_request_watcher_1.HttpRequestWatcher, isInterceptor: true },
200
- { type: entry_type_enum_1.EntryType.QUERY, watcher: query_watcher_1.QueryWatcher },
201
- { type: entry_type_enum_1.EntryType.LOG, watcher: log_watcher_1.LogWatcher },
202
- { type: entry_type_enum_1.EntryType.EXCEPTION, watcher: exception_watcher_1.ExceptionWatcher, isInterceptor: true },
203
- { type: entry_type_enum_1.EntryType.REDIS, watcher: redis_watcher_1.RedisWatcher },
204
- ];
205
- for (const item of watchersToRegister) {
206
- if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(item.type)) {
207
- if (item.isInterceptor) {
208
- providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: item.watcher });
209
- }
210
- else {
211
- providers.push(item.watcher);
212
- }
213
- }
214
- }
215
- }
216
- static forRootAsync(optionsFactory) {
217
- // Allows async injection of TelescopeOptions (e.g., from ConfigService)
218
- return {
219
- module: TelescopeModule_1,
220
- imports: [
221
- typeorm_1.TypeOrmModule.forFeature([telescope_entry_entity_1.TelescopeEntry]),
222
- jwt_1.JwtModule.registerAsync({
223
- useFactory: async (options) => ({
224
- secret: options.jwtSecret || process.env.TELESCOPE_JWT_SECRET || constants_1.DEFAULT_JWT_SECRET,
225
- signOptions: { expiresIn: '1d' },
226
- }),
227
- inject: [optionsFactory],
228
- }),
229
- ],
230
- providers: [],
231
- controllers: [telescope_controller_1.TelescopeController],
232
- };
325
+ catch (e) { }
233
326
  }
234
327
  };
235
328
  exports.TelescopeModule = TelescopeModule;
236
329
  exports.TelescopeModule = TelescopeModule = TelescopeModule_1 = __decorate([
237
330
  (0, common_1.Global)(),
238
331
  (0, common_1.Module)({}),
239
- __metadata("design:paramtypes", [core_1.ModuleRef])
332
+ __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
333
+ __metadata("design:paramtypes", [core_1.ModuleRef, Object])
240
334
  ], TelescopeModule);
@@ -1,4 +1,5 @@
1
1
  import { OnModuleInit } from '@nestjs/common';
2
+ import { HttpAdapterHost } from '@nestjs/core';
2
3
  import { TelescopeRepository } from './storage/telescope-repository.service';
3
4
  import { TelescopeEntry } from './storage/entities/telescope-entry.entity';
4
5
  import { EntryType } from './enums/entry-type.enum';
@@ -6,10 +7,15 @@ import { TelescopeOptions } from './interfaces/telescope-options.interface';
6
7
  export declare class TelescopeService implements OnModuleInit {
7
8
  private readonly repo;
8
9
  private readonly options?;
10
+ private readonly adapterHost?;
9
11
  private lastPruneTime;
10
12
  private readonly asyncLocalStorage;
11
- constructor(repo: TelescopeRepository, options?: TelescopeOptions | undefined);
13
+ constructor(repo: TelescopeRepository, options?: TelescopeOptions | undefined, adapterHost?: HttpAdapterHost | undefined);
12
14
  onModuleInit(): Promise<void>;
15
+ /**
16
+ * Get the full URL to the Telescope dashboard.
17
+ */
18
+ getDashboardUrl(): string;
13
19
  /**
14
20
  * Check if a specific entry type is enabled in the options.
15
21
  * If `enabledEntryTypes` is not configured, all types are enabled.
@@ -16,20 +16,52 @@ exports.TelescopeService = void 0;
16
16
  // src/telescope.service.ts
17
17
  const common_1 = require("@nestjs/common");
18
18
  const async_hooks_1 = require("async_hooks");
19
+ const core_1 = require("@nestjs/core");
19
20
  const telescope_repository_service_1 = require("./storage/telescope-repository.service");
20
21
  const constants_1 = require("./constants");
21
22
  let TelescopeService = class TelescopeService {
22
23
  repo;
23
24
  options;
25
+ adapterHost;
24
26
  lastPruneTime = 0;
25
27
  asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
26
- constructor(repo, options) {
28
+ constructor(repo, options, adapterHost) {
27
29
  this.repo = repo;
28
30
  this.options = options;
31
+ this.adapterHost = adapterHost;
29
32
  }
30
33
  async onModuleInit() {
31
34
  // Module is initialized; TypeORM will handle schema sync
32
35
  }
36
+ /**
37
+ * Get the full URL to the Telescope dashboard.
38
+ */
39
+ getDashboardUrl() {
40
+ const path = this.options?.path || 'telescope';
41
+ if (!this.adapterHost) {
42
+ return `/${path}`;
43
+ }
44
+ try {
45
+ const httpAdapter = this.adapterHost.httpAdapter;
46
+ if (!httpAdapter) {
47
+ return `/${path}`;
48
+ }
49
+ const instance = httpAdapter.getInstance();
50
+ if (instance && typeof instance.address === 'function') {
51
+ const address = instance.address();
52
+ if (address) {
53
+ const host = typeof address === 'string' ? address : address.address === '::' ? 'localhost' : address.address;
54
+ const port = typeof address === 'string' ? '' : `:${address.port}`;
55
+ const protocol = 'http';
56
+ return `${protocol}://${host}${port}/${path}`;
57
+ }
58
+ }
59
+ }
60
+ catch (e) {
61
+ // Fallback
62
+ }
63
+ return `/${path}`;
64
+ }
33
65
  /**
34
66
  * Check if a specific entry type is enabled in the options.
35
67
  * If `enabledEntryTypes` is not configured, all types are enabled.
@@ -110,5 +142,6 @@ exports.TelescopeService = TelescopeService = __decorate([
110
142
  (0, common_1.Injectable)(),
111
143
  __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
112
144
  __param(1, (0, common_1.Optional)()),
113
- __metadata("design:paramtypes", [telescope_repository_service_1.TelescopeRepository, Object])
145
+ __param(2, (0, common_1.Optional)()),
146
+ __metadata("design:paramtypes", [telescope_repository_service_1.TelescopeRepository, Object, core_1.HttpAdapterHost])
114
147
  ], TelescopeService);
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@ngn-net/nestjs-telescope",
3
+ "version": "0.2.0",
4
+ "builtAt": "2026-06-08T11:00:55.375Z"
5
+ }
@@ -0,0 +1,11 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ import { TelescopeService } from '../telescope.service';
3
+ import { TelescopeOptions } from '../interfaces/telescope-options.interface';
4
+ export declare class HttpClientWatcher implements OnModuleInit {
5
+ private readonly telescope;
6
+ private readonly options?;
7
+ private patched;
8
+ constructor(telescope: TelescopeService, options?: TelescopeOptions | undefined);
9
+ onModuleInit(): void;
10
+ private patch;
11
+ }
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ 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;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.HttpClientWatcher = void 0;
49
+ // src/watchers/http-client.watcher.ts
50
+ const common_1 = require("@nestjs/common");
51
+ const http = __importStar(require("http"));
52
+ const https = __importStar(require("https"));
53
+ const telescope_service_1 = require("../telescope.service");
54
+ const entry_type_enum_1 = require("../enums/entry-type.enum");
55
+ const constants_1 = require("../constants");
56
+ let HttpClientWatcher = class HttpClientWatcher {
57
+ telescope;
58
+ options;
59
+ patched = false;
60
+ constructor(telescope, options) {
61
+ this.telescope = telescope;
62
+ this.options = options;
63
+ }
64
+ onModuleInit() {
65
+ if (this.patched)
66
+ return;
67
+ this.patched = true;
68
+ this.patch(http);
69
+ this.patch(https);
70
+ }
71
+ patch(module) {
72
+ const originalRequest = module.request.bind(module);
73
+ const self = this;
74
+ // Override module.request
75
+ module.request = function (...args) {
76
+ const req = originalRequest(...args);
77
+ const startTime = Date.now();
78
+ // Resolve URL and method
79
+ let urlStr = '';
80
+ let method = 'GET';
81
+ try {
82
+ const firstArg = args[0];
83
+ if (typeof firstArg === 'string') {
84
+ urlStr = firstArg;
85
+ }
86
+ else if (firstArg instanceof URL) {
87
+ urlStr = firstArg.toString();
88
+ }
89
+ else if (typeof firstArg === 'object' && firstArg !== null) {
90
+ const { protocol, hostname, host, port, path: urlPath } = firstArg;
91
+ const proto = protocol || 'http:';
92
+ const h = hostname || host || 'localhost';
93
+ const p = port ? `:${port}` : '';
94
+ urlStr = `${proto}//${h}${p}${urlPath || '/'}`;
95
+ method = (firstArg.method || 'GET').toUpperCase();
96
+ }
97
+ }
98
+ catch {
99
+ urlStr = '';
100
+ }
101
+ // Ignore telescope's own internal paths and configured ignore list
102
+ if (self.telescope.shouldIgnorePath(urlStr)) {
103
+ return req;
104
+ }
105
+ // Skip if this type is disabled
106
+ if (!self.telescope.isEnabled(entry_type_enum_1.EntryType.HTTP_CLIENT)) {
107
+ return req;
108
+ }
109
+ // Capture request body chunks written
110
+ const reqChunks = [];
111
+ const originalWrite = req.write.bind(req);
112
+ const originalEnd = req.end.bind(req);
113
+ req.write = function (chunk, ...rest) {
114
+ if (chunk)
115
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
116
+ return originalWrite(chunk, ...rest);
117
+ };
118
+ req.end = function (chunk, ...rest) {
119
+ if (chunk)
120
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
121
+ return originalEnd(chunk, ...rest);
122
+ };
123
+ // Capture request headers snapshot after end
124
+ req.on('finish', () => {
125
+ // headers captured at response time
126
+ });
127
+ req.on('response', (res) => {
128
+ const resChunks = [];
129
+ res.on('data', (chunk) => {
130
+ resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
131
+ });
132
+ res.on('end', () => {
133
+ const duration = Date.now() - startTime;
134
+ const requestBodyRaw = Buffer.concat(reqChunks).toString('utf8');
135
+ const responseBodyRaw = Buffer.concat(resChunks).toString('utf8');
136
+ let requestBody = requestBodyRaw;
137
+ let responseBody = responseBodyRaw;
138
+ try {
139
+ requestBody = JSON.parse(requestBodyRaw);
140
+ }
141
+ catch { /* keep raw */ }
142
+ try {
143
+ responseBody = JSON.parse(responseBodyRaw);
144
+ }
145
+ catch { /* keep raw */ }
146
+ // Sanitize headers — remove Authorization tokens from logs
147
+ const reqHeaders = { ...req.getHeaders?.() };
148
+ if (reqHeaders['authorization']) {
149
+ reqHeaders['authorization'] = '[REDACTED]';
150
+ }
151
+ self.telescope.record({
152
+ type: entry_type_enum_1.EntryType.HTTP_CLIENT,
153
+ content: {
154
+ method: method || (req.method || 'GET').toUpperCase(),
155
+ url: urlStr,
156
+ requestHeaders: reqHeaders,
157
+ requestBody,
158
+ responseStatus: res.statusCode,
159
+ responseHeaders: res.headers,
160
+ responseBody,
161
+ duration,
162
+ },
163
+ }).catch(() => { });
164
+ });
165
+ res.on('error', () => { });
166
+ });
167
+ req.on('error', (err) => {
168
+ const duration = Date.now() - startTime;
169
+ self.telescope.record({
170
+ type: entry_type_enum_1.EntryType.HTTP_CLIENT,
171
+ content: {
172
+ method,
173
+ url: urlStr,
174
+ requestHeaders: {},
175
+ requestBody: null,
176
+ responseStatus: 0,
177
+ responseBody: null,
178
+ error: err.message,
179
+ duration,
180
+ },
181
+ }).catch(() => { });
182
+ });
183
+ return req;
184
+ };
185
+ // Also handle module.get (convenience method)
186
+ const originalGet = module.get.bind(module);
187
+ module.get = function (...args) {
188
+ const req = module.request(...args);
189
+ req.end();
190
+ return req;
191
+ };
192
+ }
193
+ };
194
+ exports.HttpClientWatcher = HttpClientWatcher;
195
+ exports.HttpClientWatcher = HttpClientWatcher = __decorate([
196
+ (0, common_1.Injectable)(),
197
+ __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
198
+ __param(1, (0, common_1.Optional)()),
199
+ __metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object])
200
+ ], HttpClientWatcher);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngn-net/nestjs-telescope",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -73,16 +73,21 @@
73
73
  "@nestjs/event-emitter": "^2",
74
74
  "@nestjs/jwt": "^11",
75
75
  "@nestjs/passport": "^11",
76
+ "@nestjs/platform-express": "^11.1.25",
76
77
  "@nestjs/schedule": "^4",
78
+ "@nestjs/testing": "^11.1.25",
77
79
  "@nestjs/typeorm": "^11",
78
80
  "@types/express": "^4.17.21",
79
81
  "@types/jest": "^29",
80
82
  "@types/node": "^20",
81
83
  "@types/nodemailer": "^6.4.15",
84
+ "@types/supertest": "^7.2.0",
82
85
  "jest": "^29",
83
86
  "nodemailer": "^6",
84
87
  "rimraf": "^5",
85
88
  "rxjs": "^7",
89
+ "sqlite3": "^6.0.1",
90
+ "supertest": "^7.2.2",
86
91
  "ts-jest": "^29",
87
92
  "ts-node": "^10",
88
93
  "typeorm": "^0.3",