@ngn-net/nestjs-telescope 0.2.3 → 0.2.5

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/dist/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from './watchers/command.watcher';
12
12
  export * from './watchers/model.watcher';
13
13
  export * from './watchers/notification.watcher';
14
14
  export * from './watchers/gate.watcher';
15
+ export * from './watchers/http-client.watcher';
package/dist/index.js CHANGED
@@ -30,3 +30,4 @@ __exportStar(require("./watchers/command.watcher"), exports);
30
30
  __exportStar(require("./watchers/model.watcher"), exports);
31
31
  __exportStar(require("./watchers/notification.watcher"), exports);
32
32
  __exportStar(require("./watchers/gate.watcher"), exports);
33
+ __exportStar(require("./watchers/http-client.watcher"), exports);
@@ -25,6 +25,7 @@ const telescope_controller_1 = require("./controllers/telescope.controller");
25
25
  const telescope_repository_service_1 = require("./storage/telescope-repository.service");
26
26
  const telescope_entry_entity_1 = require("./storage/entities/telescope-entry.entity");
27
27
  const http_request_watcher_1 = require("./watchers/http-request.watcher");
28
+ const http_client_watcher_1 = require("./watchers/http-client.watcher");
28
29
  const query_watcher_1 = require("./watchers/query.watcher");
29
30
  const cache_watcher_1 = require("./watchers/cache.watcher");
30
31
  const queue_watcher_1 = require("./watchers/queue.watcher");
@@ -238,6 +239,7 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
238
239
  // Additional optional watchers (non-interceptors)
239
240
  if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.HTTP_CLIENT)) {
240
241
  providers.push(http_service_watcher_1.HttpServiceWatcher);
242
+ providers.push(http_client_watcher_1.HttpClientWatcher);
241
243
  }
242
244
  if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.COMMAND)) {
243
245
  providers.push(command_watcher_1.CommandWatcher);
@@ -310,6 +312,8 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
310
312
  providers.push(query_watcher_1.QueryWatcher);
311
313
  providers.push(log_watcher_1.LogWatcher);
312
314
  providers.push(redis_watcher_1.RedisWatcher);
315
+ providers.push(http_client_watcher_1.HttpClientWatcher);
316
+ providers.push(http_service_watcher_1.HttpServiceWatcher);
313
317
  // Optional modules/watchers if packages are present
314
318
  try {
315
319
  require('@nestjs/cache-manager');
@@ -487,6 +487,7 @@
487
487
  }
488
488
 
489
489
  .badge-request { background-color: rgba(59, 130, 246, 0.12); color: var(--accent-blue); }
490
+ .badge-http_client { background-color: rgba(99, 102, 241, 0.12); color: var(--primary); }
490
491
  .badge-query { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
491
492
  .badge-cache { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
492
493
  .badge-job { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
@@ -496,6 +497,11 @@
496
497
  .badge-exception { background-color: rgba(244, 63, 94, 0.12); color: var(--accent-red); }
497
498
  .badge-scheduled_task { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
498
499
  .badge-redis { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
500
+ .badge-gate { background-color: rgba(244, 63, 94, 0.12); color: var(--accent-red); }
501
+ .badge-command { background-color: rgba(148, 163, 184, 0.15); color: var(--text-main); }
502
+ .badge-model { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
503
+ .badge-notification { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
504
+ .badge-dump { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
499
505
 
500
506
  .badge-get { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
501
507
  .badge-post { background-color: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
@@ -845,6 +851,7 @@
845
851
  const NAVIGATION_TABS = [
846
852
  { id: 'all', label: 'All Entries', icon: '🔍' },
847
853
  { id: 'request', label: 'HTTP Requests', icon: '🌐' },
854
+ { id: 'http_client', label: 'HTTP Client Requests', icon: '📞' },
848
855
  { id: 'query', label: 'Database Queries', icon: '💾' },
849
856
  { id: 'cache', label: 'Cache Ops', icon: '⚡' },
850
857
  { id: 'job', label: 'Queue Jobs', icon: '⚙️' },
@@ -854,6 +861,11 @@
854
861
  { id: 'exception', label: 'Exceptions', icon: '❌' },
855
862
  { id: 'scheduled_task', label: 'Scheduled Tasks', icon: '⏱️' },
856
863
  { id: 'redis', label: 'Redis Commands', icon: '🔑' },
864
+ { id: 'gate', label: 'Gates', icon: '🚪' },
865
+ { id: 'command', label: 'Commands', icon: '💻' },
866
+ { id: 'model', label: 'Models', icon: '📦' },
867
+ { id: 'notification', label: 'Notifications', icon: '🔔' },
868
+ { id: 'dump', label: 'Dumps', icon: '🗑️' },
857
869
  ];
858
870
 
859
871
  function App() {
@@ -1151,7 +1163,7 @@
1151
1163
  {/* Stats Grid at the top for quick insights */}
1152
1164
  {activeTab === 'all' && (
1153
1165
  <div className="stats-grid">
1154
- {NAVIGATION_TABS.slice(1, 7).map((tab) => (
1166
+ {NAVIGATION_TABS.slice(1).map((tab) => (
1155
1167
  <div
1156
1168
  key={tab.id}
1157
1169
  className={`stat-card ${activeTab === tab.id ? 'active' : ''}`}
@@ -1324,7 +1336,7 @@
1324
1336
 
1325
1337
  // Determine font family for subtitle
1326
1338
  function varMono(type) {
1327
- return (type === 'query' || type === 'redis' || type === 'request') ? 'var(--font-mono)' : 'var(--font-sans)';
1339
+ return (type === 'query' || type === 'redis' || type === 'request' || type === 'http_client' || type === 'command') ? 'var(--font-mono)' : 'var(--font-sans)';
1328
1340
  }
1329
1341
 
1330
1342
  // Helper functions for displaying table items
@@ -1345,6 +1357,16 @@
1345
1357
  subtitle: `${req.url || '/'}`,
1346
1358
  status: statusBadge
1347
1359
  };
1360
+ case 'http_client':
1361
+ return {
1362
+ title: `${content.method || 'GET'}`,
1363
+ subtitle: `${content.url || ''} (${content.duration || 0}ms)`,
1364
+ status: content.responseStatus >= 400 ? (
1365
+ <span className="badge badge-status-err">{content.responseStatus}</span>
1366
+ ) : (
1367
+ <span className="badge badge-status-ok">{content.responseStatus || '200'}</span>
1368
+ )
1369
+ };
1348
1370
  case 'query':
1349
1371
  return {
1350
1372
  title: content.query || 'DB Query',
@@ -1419,6 +1441,40 @@
1419
1441
  <span className="badge badge-status-ok">success</span>
1420
1442
  )
1421
1443
  };
1444
+ case 'gate':
1445
+ return {
1446
+ title: `Gate: ${content.gate || ''}`,
1447
+ subtitle: content.user ? `User: ${JSON.stringify(content.user)}` : 'Anonymous',
1448
+ status: content.allowed ? (
1449
+ <span className="badge badge-status-ok">allowed</span>
1450
+ ) : (
1451
+ <span className="badge badge-status-err">denied</span>
1452
+ )
1453
+ };
1454
+ case 'command':
1455
+ return {
1456
+ title: `Command: ${content.command || ''}`,
1457
+ subtitle: (content.args || []).join(' '),
1458
+ status: <span className="badge badge-status-ok">{content.exitCode ?? 'executed'}</span>
1459
+ };
1460
+ case 'model':
1461
+ return {
1462
+ title: `${(content.action || '').toUpperCase()} model`,
1463
+ subtitle: `${content.entity || ''} (${content.primaryKey || ''})`,
1464
+ status: <span className="badge badge-status-ok">{content.action}</span>
1465
+ };
1466
+ case 'notification':
1467
+ return {
1468
+ title: `Notification: ${content.notification || ''}`,
1469
+ subtitle: content.timestamp || '',
1470
+ status: <span className="badge badge-status-ok">sent</span>
1471
+ };
1472
+ case 'dump':
1473
+ return {
1474
+ title: `Dump`,
1475
+ subtitle: typeof content.message === 'string' ? content.message : JSON.stringify(content.message),
1476
+ status: <span className="badge badge-status-ok">dumped</span>
1477
+ };
1422
1478
  default:
1423
1479
  return {
1424
1480
  title: 'Telescope Entry',
@@ -1471,6 +1527,59 @@
1471
1527
  )}
1472
1528
  </React.Fragment>
1473
1529
  );
1530
+ case 'http_client':
1531
+ return (
1532
+ <React.Fragment>
1533
+ <div className="key-val-row">
1534
+ <div className="key-val-label">HTTP Method</div>
1535
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.method}</div>
1536
+ </div>
1537
+ <div className="key-val-row">
1538
+ <div className="key-val-label">URL</div>
1539
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)', color: '#818cf8' }}>{content.url}</div>
1540
+ </div>
1541
+ <div className="key-val-row">
1542
+ <div className="key-val-label">Duration</div>
1543
+ <div className="key-val-value">{content.duration} ms</div>
1544
+ </div>
1545
+ {content.requestHeaders && Object.keys(content.requestHeaders).length > 0 && (
1546
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1547
+ <div className="key-val-label">Request Headers</div>
1548
+ <pre className="json-code">{JSON.stringify(content.requestHeaders, null, 2)}</pre>
1549
+ </div>
1550
+ )}
1551
+ {content.requestBody && (
1552
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1553
+ <div className="key-val-label">Request Body</div>
1554
+ <pre className="json-code">{JSON.stringify(content.requestBody, null, 2)}</pre>
1555
+ </div>
1556
+ )}
1557
+ {content.responseStatus !== undefined && (
1558
+ <div className="key-val-row">
1559
+ <div className="key-val-label">Response Status</div>
1560
+ <div className="key-val-value">{content.responseStatus}</div>
1561
+ </div>
1562
+ )}
1563
+ {content.responseHeaders && Object.keys(content.responseHeaders).length > 0 && (
1564
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1565
+ <div className="key-val-label">Response Headers</div>
1566
+ <pre className="json-code">{JSON.stringify(content.responseHeaders, null, 2)}</pre>
1567
+ </div>
1568
+ )}
1569
+ {content.responseBody && (
1570
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1571
+ <div className="key-val-label">Response Body</div>
1572
+ <pre className="json-code">{JSON.stringify(content.responseBody, null, 2)}</pre>
1573
+ </div>
1574
+ )}
1575
+ {content.error && (
1576
+ <div className="key-val-row">
1577
+ <div className="key-val-label">Error</div>
1578
+ <div className="key-val-value" style={{ color: 'var(--accent-red)' }}>{content.error}</div>
1579
+ </div>
1580
+ )}
1581
+ </React.Fragment>
1582
+ );
1474
1583
  case 'query':
1475
1584
  return (
1476
1585
  <React.Fragment>
@@ -1617,6 +1726,105 @@
1617
1726
  )}
1618
1727
  </React.Fragment>
1619
1728
  );
1729
+ case 'gate':
1730
+ return (
1731
+ <React.Fragment>
1732
+ <div className="key-val-row">
1733
+ <div className="key-val-label">Gate Name</div>
1734
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.gate}</div>
1735
+ </div>
1736
+ <div className="key-val-row">
1737
+ <div className="key-val-label">Result</div>
1738
+ <div className="key-val-value">
1739
+ {content.allowed ? (
1740
+ <span className="badge badge-status-ok">Allowed</span>
1741
+ ) : (
1742
+ <span className="badge badge-status-err">Denied</span>
1743
+ )}
1744
+ </div>
1745
+ </div>
1746
+ {content.user && (
1747
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1748
+ <div className="key-val-label">User Context</div>
1749
+ <pre className="json-code">{JSON.stringify(content.user, null, 2)}</pre>
1750
+ </div>
1751
+ )}
1752
+ </React.Fragment>
1753
+ );
1754
+ case 'command':
1755
+ return (
1756
+ <React.Fragment>
1757
+ <div className="key-val-row">
1758
+ <div className="key-val-label">Command</div>
1759
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff', fontFamily: 'var(--font-mono)' }}>{content.command}</div>
1760
+ </div>
1761
+ {content.args && content.args.length > 0 && (
1762
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1763
+ <div className="key-val-label">Arguments</div>
1764
+ <pre className="json-code">{JSON.stringify(content.args, null, 2)}</pre>
1765
+ </div>
1766
+ )}
1767
+ {content.exitCode !== undefined && (
1768
+ <div className="key-val-row">
1769
+ <div className="key-val-label">Exit Code</div>
1770
+ <div className="key-val-value">{content.exitCode}</div>
1771
+ </div>
1772
+ )}
1773
+ </React.Fragment>
1774
+ );
1775
+ case 'model':
1776
+ return (
1777
+ <React.Fragment>
1778
+ <div className="key-val-row">
1779
+ <div className="key-val-label">Action</div>
1780
+ <div className="key-val-value" style={{ textTransform: 'uppercase', fontWeight: '600', color: '#fff' }}>{content.action}</div>
1781
+ </div>
1782
+ <div className="key-val-row">
1783
+ <div className="key-val-label">Entity</div>
1784
+ <div className="key-val-value">{content.entity}</div>
1785
+ </div>
1786
+ <div className="key-val-row">
1787
+ <div className="key-val-label">Primary Key</div>
1788
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{String(content.primaryKey)}</div>
1789
+ </div>
1790
+ {content.updatedColumns && content.updatedColumns.length > 0 && (
1791
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1792
+ <div className="key-val-label">Updated Columns</div>
1793
+ <pre className="json-code">{JSON.stringify(content.updatedColumns, null, 2)}</pre>
1794
+ </div>
1795
+ )}
1796
+ {content.data && (
1797
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1798
+ <div className="key-val-label">Model Data</div>
1799
+ <pre className="json-code">{JSON.stringify(content.data, null, 2)}</pre>
1800
+ </div>
1801
+ )}
1802
+ </React.Fragment>
1803
+ );
1804
+ case 'notification':
1805
+ return (
1806
+ <React.Fragment>
1807
+ <div className="key-val-row">
1808
+ <div className="key-val-label">Notification</div>
1809
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.notification}</div>
1810
+ </div>
1811
+ {content.payload && (
1812
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1813
+ <div className="key-val-label">Payload</div>
1814
+ <pre className="json-code">{JSON.stringify(content.payload, null, 2)}</pre>
1815
+ </div>
1816
+ )}
1817
+ </React.Fragment>
1818
+ );
1819
+ case 'dump':
1820
+ return (
1821
+ <React.Fragment>
1822
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1823
+ <div className="key-val-label">Message / Data</div>
1824
+ <pre className="json-code">{typeof content.message === 'string' ? content.message : JSON.stringify(content.message, null, 2)}</pre>
1825
+ </div>
1826
+ </React.Fragment>
1827
+ );
1620
1828
  default:
1621
1829
  return (
1622
1830
  <div className="key-val-row">
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ngn-net/nestjs-telescope",
3
- "version": "0.2.3",
4
- "builtAt": "2026-06-08T11:52:38.806Z"
3
+ "version": "0.2.5",
4
+ "builtAt": "2026-06-08T12:21:55.387Z"
5
5
  }
@@ -69,126 +69,167 @@ let HttpClientWatcher = class HttpClientWatcher {
69
69
  this.patch(https);
70
70
  }
71
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;
72
+ try {
73
+ const originalRequest = module.request;
74
+ if (typeof originalRequest !== 'function')
75
+ return;
76
+ const originalRequestBound = originalRequest.bind(module);
77
+ const self = this;
78
+ const customRequest = function (...args) {
79
+ const req = originalRequestBound(...args);
80
+ const startTime = Date.now();
81
+ // Resolve URL and method
82
+ let urlStr = '';
83
+ let method = 'GET';
84
+ try {
85
+ const firstArg = args[0];
86
+ if (typeof firstArg === 'string') {
87
+ urlStr = firstArg;
88
+ }
89
+ else if (firstArg instanceof URL) {
90
+ urlStr = firstArg.toString();
91
+ }
92
+ else if (typeof firstArg === 'object' && firstArg !== null) {
93
+ const { protocol, hostname, host, port, path: urlPath } = firstArg;
94
+ const proto = protocol || 'http:';
95
+ const h = hostname || host || 'localhost';
96
+ const p = port ? `:${port}` : '';
97
+ urlStr = `${proto}//${h}${p}${urlPath || '/'}`;
98
+ method = (firstArg.method || 'GET').toUpperCase();
99
+ }
85
100
  }
86
- else if (firstArg instanceof URL) {
87
- urlStr = firstArg.toString();
101
+ catch {
102
+ urlStr = '';
88
103
  }
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();
104
+ // Ignore telescope's own internal paths and configured ignore list
105
+ if (self.telescope.shouldIgnorePath(urlStr)) {
106
+ return req;
96
107
  }
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));
108
+ // Skip if this type is disabled
109
+ if (!self.telescope.isEnabled(entry_type_enum_1.EntryType.HTTP_CLIENT)) {
110
+ return req;
111
+ }
112
+ // Capture request body chunks written
113
+ const reqChunks = [];
114
+ const originalWrite = req.write.bind(req);
115
+ const originalEnd = req.end.bind(req);
116
+ req.write = function (chunk, ...rest) {
117
+ if (chunk)
118
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
119
+ return originalWrite(chunk, ...rest);
120
+ };
121
+ req.end = function (chunk, ...rest) {
122
+ if (chunk)
123
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
124
+ return originalEnd(chunk, ...rest);
125
+ };
126
+ req.on('response', (res) => {
127
+ const resChunks = [];
128
+ res.on('data', (chunk) => {
129
+ resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
130
+ });
131
+ res.on('end', () => {
132
+ const duration = Date.now() - startTime;
133
+ const requestBodyRaw = Buffer.concat(reqChunks).toString('utf8');
134
+ const responseBodyRaw = Buffer.concat(resChunks).toString('utf8');
135
+ let requestBody = requestBodyRaw;
136
+ let responseBody = responseBodyRaw;
137
+ try {
138
+ requestBody = JSON.parse(requestBodyRaw);
139
+ }
140
+ catch { /* keep raw */ }
141
+ try {
142
+ responseBody = JSON.parse(responseBodyRaw);
143
+ }
144
+ catch { /* keep raw */ }
145
+ // Sanitize headers — remove Authorization tokens from logs
146
+ const reqHeaders = { ...req.getHeaders?.() };
147
+ if (reqHeaders['authorization']) {
148
+ reqHeaders['authorization'] = '[REDACTED]';
149
+ }
150
+ self.telescope.record({
151
+ type: entry_type_enum_1.EntryType.HTTP_CLIENT,
152
+ content: {
153
+ method: method || (req.method || 'GET').toUpperCase(),
154
+ url: urlStr,
155
+ requestHeaders: reqHeaders,
156
+ requestBody,
157
+ responseStatus: res.statusCode,
158
+ responseHeaders: res.headers,
159
+ responseBody,
160
+ duration,
161
+ },
162
+ }).catch(() => { });
163
+ });
164
+ res.on('error', () => { });
131
165
  });
132
- res.on('end', () => {
166
+ req.on('error', (err) => {
133
167
  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
168
  self.telescope.record({
152
169
  type: entry_type_enum_1.EntryType.HTTP_CLIENT,
153
170
  content: {
154
- method: method || (req.method || 'GET').toUpperCase(),
171
+ method,
155
172
  url: urlStr,
156
- requestHeaders: reqHeaders,
157
- requestBody,
158
- responseStatus: res.statusCode,
159
- responseHeaders: res.headers,
160
- responseBody,
173
+ requestHeaders: {},
174
+ requestBody: null,
175
+ responseStatus: 0,
176
+ responseBody: null,
177
+ error: err.message,
161
178
  duration,
162
179
  },
163
180
  }).catch(() => { });
164
181
  });
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
- };
182
+ return req;
183
+ };
184
+ try {
185
+ const desc = Object.getOwnPropertyDescriptor(module, 'request');
186
+ if (desc && desc.configurable === false) {
187
+ // Can't redefine using defineProperty, try simple assignment
188
+ module.request = customRequest;
189
+ }
190
+ else {
191
+ Object.defineProperty(module, 'request', {
192
+ value: customRequest,
193
+ configurable: true,
194
+ writable: true,
195
+ });
196
+ }
197
+ }
198
+ catch {
199
+ try {
200
+ module.request = customRequest;
201
+ }
202
+ catch { }
203
+ }
204
+ const customGet = function (...args) {
205
+ const req = module.request(...args);
206
+ req.end();
207
+ return req;
208
+ };
209
+ try {
210
+ const desc = Object.getOwnPropertyDescriptor(module, 'get');
211
+ if (desc && desc.configurable === false) {
212
+ // Can't redefine using defineProperty, try simple assignment
213
+ module.get = customGet;
214
+ }
215
+ else {
216
+ Object.defineProperty(module, 'get', {
217
+ value: customGet,
218
+ configurable: true,
219
+ writable: true,
220
+ });
221
+ }
222
+ }
223
+ catch {
224
+ try {
225
+ module.get = customGet;
226
+ }
227
+ catch { }
228
+ }
229
+ }
230
+ catch (e) {
231
+ // Prevent any crash
232
+ }
192
233
  }
193
234
  };
194
235
  exports.HttpClientWatcher = HttpClientWatcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngn-net/nestjs-telescope",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/ui/index.html CHANGED
@@ -487,6 +487,7 @@
487
487
  }
488
488
 
489
489
  .badge-request { background-color: rgba(59, 130, 246, 0.12); color: var(--accent-blue); }
490
+ .badge-http_client { background-color: rgba(99, 102, 241, 0.12); color: var(--primary); }
490
491
  .badge-query { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
491
492
  .badge-cache { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
492
493
  .badge-job { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
@@ -496,6 +497,11 @@
496
497
  .badge-exception { background-color: rgba(244, 63, 94, 0.12); color: var(--accent-red); }
497
498
  .badge-scheduled_task { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
498
499
  .badge-redis { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
500
+ .badge-gate { background-color: rgba(244, 63, 94, 0.12); color: var(--accent-red); }
501
+ .badge-command { background-color: rgba(148, 163, 184, 0.15); color: var(--text-main); }
502
+ .badge-model { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
503
+ .badge-notification { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
504
+ .badge-dump { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
499
505
 
500
506
  .badge-get { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
501
507
  .badge-post { background-color: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
@@ -845,6 +851,7 @@
845
851
  const NAVIGATION_TABS = [
846
852
  { id: 'all', label: 'All Entries', icon: '🔍' },
847
853
  { id: 'request', label: 'HTTP Requests', icon: '🌐' },
854
+ { id: 'http_client', label: 'HTTP Client Requests', icon: '📞' },
848
855
  { id: 'query', label: 'Database Queries', icon: '💾' },
849
856
  { id: 'cache', label: 'Cache Ops', icon: '⚡' },
850
857
  { id: 'job', label: 'Queue Jobs', icon: '⚙️' },
@@ -854,6 +861,11 @@
854
861
  { id: 'exception', label: 'Exceptions', icon: '❌' },
855
862
  { id: 'scheduled_task', label: 'Scheduled Tasks', icon: '⏱️' },
856
863
  { id: 'redis', label: 'Redis Commands', icon: '🔑' },
864
+ { id: 'gate', label: 'Gates', icon: '🚪' },
865
+ { id: 'command', label: 'Commands', icon: '💻' },
866
+ { id: 'model', label: 'Models', icon: '📦' },
867
+ { id: 'notification', label: 'Notifications', icon: '🔔' },
868
+ { id: 'dump', label: 'Dumps', icon: '🗑️' },
857
869
  ];
858
870
 
859
871
  function App() {
@@ -1151,7 +1163,7 @@
1151
1163
  {/* Stats Grid at the top for quick insights */}
1152
1164
  {activeTab === 'all' && (
1153
1165
  <div className="stats-grid">
1154
- {NAVIGATION_TABS.slice(1, 7).map((tab) => (
1166
+ {NAVIGATION_TABS.slice(1).map((tab) => (
1155
1167
  <div
1156
1168
  key={tab.id}
1157
1169
  className={`stat-card ${activeTab === tab.id ? 'active' : ''}`}
@@ -1324,7 +1336,7 @@
1324
1336
 
1325
1337
  // Determine font family for subtitle
1326
1338
  function varMono(type) {
1327
- return (type === 'query' || type === 'redis' || type === 'request') ? 'var(--font-mono)' : 'var(--font-sans)';
1339
+ return (type === 'query' || type === 'redis' || type === 'request' || type === 'http_client' || type === 'command') ? 'var(--font-mono)' : 'var(--font-sans)';
1328
1340
  }
1329
1341
 
1330
1342
  // Helper functions for displaying table items
@@ -1345,6 +1357,16 @@
1345
1357
  subtitle: `${req.url || '/'}`,
1346
1358
  status: statusBadge
1347
1359
  };
1360
+ case 'http_client':
1361
+ return {
1362
+ title: `${content.method || 'GET'}`,
1363
+ subtitle: `${content.url || ''} (${content.duration || 0}ms)`,
1364
+ status: content.responseStatus >= 400 ? (
1365
+ <span className="badge badge-status-err">{content.responseStatus}</span>
1366
+ ) : (
1367
+ <span className="badge badge-status-ok">{content.responseStatus || '200'}</span>
1368
+ )
1369
+ };
1348
1370
  case 'query':
1349
1371
  return {
1350
1372
  title: content.query || 'DB Query',
@@ -1419,6 +1441,40 @@
1419
1441
  <span className="badge badge-status-ok">success</span>
1420
1442
  )
1421
1443
  };
1444
+ case 'gate':
1445
+ return {
1446
+ title: `Gate: ${content.gate || ''}`,
1447
+ subtitle: content.user ? `User: ${JSON.stringify(content.user)}` : 'Anonymous',
1448
+ status: content.allowed ? (
1449
+ <span className="badge badge-status-ok">allowed</span>
1450
+ ) : (
1451
+ <span className="badge badge-status-err">denied</span>
1452
+ )
1453
+ };
1454
+ case 'command':
1455
+ return {
1456
+ title: `Command: ${content.command || ''}`,
1457
+ subtitle: (content.args || []).join(' '),
1458
+ status: <span className="badge badge-status-ok">{content.exitCode ?? 'executed'}</span>
1459
+ };
1460
+ case 'model':
1461
+ return {
1462
+ title: `${(content.action || '').toUpperCase()} model`,
1463
+ subtitle: `${content.entity || ''} (${content.primaryKey || ''})`,
1464
+ status: <span className="badge badge-status-ok">{content.action}</span>
1465
+ };
1466
+ case 'notification':
1467
+ return {
1468
+ title: `Notification: ${content.notification || ''}`,
1469
+ subtitle: content.timestamp || '',
1470
+ status: <span className="badge badge-status-ok">sent</span>
1471
+ };
1472
+ case 'dump':
1473
+ return {
1474
+ title: `Dump`,
1475
+ subtitle: typeof content.message === 'string' ? content.message : JSON.stringify(content.message),
1476
+ status: <span className="badge badge-status-ok">dumped</span>
1477
+ };
1422
1478
  default:
1423
1479
  return {
1424
1480
  title: 'Telescope Entry',
@@ -1471,6 +1527,59 @@
1471
1527
  )}
1472
1528
  </React.Fragment>
1473
1529
  );
1530
+ case 'http_client':
1531
+ return (
1532
+ <React.Fragment>
1533
+ <div className="key-val-row">
1534
+ <div className="key-val-label">HTTP Method</div>
1535
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.method}</div>
1536
+ </div>
1537
+ <div className="key-val-row">
1538
+ <div className="key-val-label">URL</div>
1539
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)', color: '#818cf8' }}>{content.url}</div>
1540
+ </div>
1541
+ <div className="key-val-row">
1542
+ <div className="key-val-label">Duration</div>
1543
+ <div className="key-val-value">{content.duration} ms</div>
1544
+ </div>
1545
+ {content.requestHeaders && Object.keys(content.requestHeaders).length > 0 && (
1546
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1547
+ <div className="key-val-label">Request Headers</div>
1548
+ <pre className="json-code">{JSON.stringify(content.requestHeaders, null, 2)}</pre>
1549
+ </div>
1550
+ )}
1551
+ {content.requestBody && (
1552
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1553
+ <div className="key-val-label">Request Body</div>
1554
+ <pre className="json-code">{JSON.stringify(content.requestBody, null, 2)}</pre>
1555
+ </div>
1556
+ )}
1557
+ {content.responseStatus !== undefined && (
1558
+ <div className="key-val-row">
1559
+ <div className="key-val-label">Response Status</div>
1560
+ <div className="key-val-value">{content.responseStatus}</div>
1561
+ </div>
1562
+ )}
1563
+ {content.responseHeaders && Object.keys(content.responseHeaders).length > 0 && (
1564
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1565
+ <div className="key-val-label">Response Headers</div>
1566
+ <pre className="json-code">{JSON.stringify(content.responseHeaders, null, 2)}</pre>
1567
+ </div>
1568
+ )}
1569
+ {content.responseBody && (
1570
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1571
+ <div className="key-val-label">Response Body</div>
1572
+ <pre className="json-code">{JSON.stringify(content.responseBody, null, 2)}</pre>
1573
+ </div>
1574
+ )}
1575
+ {content.error && (
1576
+ <div className="key-val-row">
1577
+ <div className="key-val-label">Error</div>
1578
+ <div className="key-val-value" style={{ color: 'var(--accent-red)' }}>{content.error}</div>
1579
+ </div>
1580
+ )}
1581
+ </React.Fragment>
1582
+ );
1474
1583
  case 'query':
1475
1584
  return (
1476
1585
  <React.Fragment>
@@ -1617,6 +1726,105 @@
1617
1726
  )}
1618
1727
  </React.Fragment>
1619
1728
  );
1729
+ case 'gate':
1730
+ return (
1731
+ <React.Fragment>
1732
+ <div className="key-val-row">
1733
+ <div className="key-val-label">Gate Name</div>
1734
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.gate}</div>
1735
+ </div>
1736
+ <div className="key-val-row">
1737
+ <div className="key-val-label">Result</div>
1738
+ <div className="key-val-value">
1739
+ {content.allowed ? (
1740
+ <span className="badge badge-status-ok">Allowed</span>
1741
+ ) : (
1742
+ <span className="badge badge-status-err">Denied</span>
1743
+ )}
1744
+ </div>
1745
+ </div>
1746
+ {content.user && (
1747
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1748
+ <div className="key-val-label">User Context</div>
1749
+ <pre className="json-code">{JSON.stringify(content.user, null, 2)}</pre>
1750
+ </div>
1751
+ )}
1752
+ </React.Fragment>
1753
+ );
1754
+ case 'command':
1755
+ return (
1756
+ <React.Fragment>
1757
+ <div className="key-val-row">
1758
+ <div className="key-val-label">Command</div>
1759
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff', fontFamily: 'var(--font-mono)' }}>{content.command}</div>
1760
+ </div>
1761
+ {content.args && content.args.length > 0 && (
1762
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1763
+ <div className="key-val-label">Arguments</div>
1764
+ <pre className="json-code">{JSON.stringify(content.args, null, 2)}</pre>
1765
+ </div>
1766
+ )}
1767
+ {content.exitCode !== undefined && (
1768
+ <div className="key-val-row">
1769
+ <div className="key-val-label">Exit Code</div>
1770
+ <div className="key-val-value">{content.exitCode}</div>
1771
+ </div>
1772
+ )}
1773
+ </React.Fragment>
1774
+ );
1775
+ case 'model':
1776
+ return (
1777
+ <React.Fragment>
1778
+ <div className="key-val-row">
1779
+ <div className="key-val-label">Action</div>
1780
+ <div className="key-val-value" style={{ textTransform: 'uppercase', fontWeight: '600', color: '#fff' }}>{content.action}</div>
1781
+ </div>
1782
+ <div className="key-val-row">
1783
+ <div className="key-val-label">Entity</div>
1784
+ <div className="key-val-value">{content.entity}</div>
1785
+ </div>
1786
+ <div className="key-val-row">
1787
+ <div className="key-val-label">Primary Key</div>
1788
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{String(content.primaryKey)}</div>
1789
+ </div>
1790
+ {content.updatedColumns && content.updatedColumns.length > 0 && (
1791
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1792
+ <div className="key-val-label">Updated Columns</div>
1793
+ <pre className="json-code">{JSON.stringify(content.updatedColumns, null, 2)}</pre>
1794
+ </div>
1795
+ )}
1796
+ {content.data && (
1797
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1798
+ <div className="key-val-label">Model Data</div>
1799
+ <pre className="json-code">{JSON.stringify(content.data, null, 2)}</pre>
1800
+ </div>
1801
+ )}
1802
+ </React.Fragment>
1803
+ );
1804
+ case 'notification':
1805
+ return (
1806
+ <React.Fragment>
1807
+ <div className="key-val-row">
1808
+ <div className="key-val-label">Notification</div>
1809
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.notification}</div>
1810
+ </div>
1811
+ {content.payload && (
1812
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1813
+ <div className="key-val-label">Payload</div>
1814
+ <pre className="json-code">{JSON.stringify(content.payload, null, 2)}</pre>
1815
+ </div>
1816
+ )}
1817
+ </React.Fragment>
1818
+ );
1819
+ case 'dump':
1820
+ return (
1821
+ <React.Fragment>
1822
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1823
+ <div className="key-val-label">Message / Data</div>
1824
+ <pre className="json-code">{typeof content.message === 'string' ? content.message : JSON.stringify(content.message, null, 2)}</pre>
1825
+ </div>
1826
+ </React.Fragment>
1827
+ );
1620
1828
  default:
1621
1829
  return (
1622
1830
  <div className="key-val-row">