@geekmidas/telescope 9.0.2 → 10.0.0-alpha.1

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.
Files changed (122) hide show
  1. package/README.md +14 -4
  2. package/dist/{http-D5kn3_Mt.d.mts → http-BiNtg85L.d.mts} +3 -3
  3. package/dist/{http-D5kn3_Mt.d.mts.map → http-BiNtg85L.d.mts.map} +1 -1
  4. package/dist/instrumentation/http.d.mts +1 -1
  5. package/dist/instrumentation/index.cjs +2 -2
  6. package/dist/instrumentation/index.d.cts +2 -2
  7. package/dist/instrumentation/index.d.mts +3 -3
  8. package/dist/instrumentation/index.mjs +2 -2
  9. package/dist/instrumentation/middleware.cjs +1 -1
  10. package/dist/instrumentation/middleware.d.cts +1 -1
  11. package/dist/instrumentation/middleware.d.mts +1 -1
  12. package/dist/instrumentation/middleware.mjs +1 -1
  13. package/dist/instrumentation/otel.cjs +1 -1
  14. package/dist/instrumentation/otel.d.cts +1 -1
  15. package/dist/instrumentation/otel.d.mts +1 -1
  16. package/dist/instrumentation/otel.mjs +1 -1
  17. package/dist/logger/console.d.cts.map +1 -1
  18. package/dist/logger/pino.d.mts +2 -2
  19. package/dist/{middleware-DqPQpcup.cjs → middleware-Bcq6XXGG.cjs} +2 -2
  20. package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
  21. package/dist/{middleware-4-GrQlOT.mjs → middleware-D1XEHvyy.mjs} +2 -2
  22. package/dist/middleware-D1XEHvyy.mjs.map +1 -0
  23. package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
  24. package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
  25. package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
  26. package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
  27. package/dist/{otel-40udNP5g.mjs → otel-B9bNoeZ2.mjs} +3 -3
  28. package/dist/otel-B9bNoeZ2.mjs.map +1 -0
  29. package/dist/{otel-SBVoVEHr.d.cts → otel-Bk89b1lX.d.cts} +3 -3
  30. package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
  31. package/dist/{otel-COrpz_GZ.cjs → otel-BzjOtVxZ.cjs} +3 -3
  32. package/dist/otel-BzjOtVxZ.cjs.map +1 -0
  33. package/dist/{otel-1bJPb2Z7.d.mts → otel-lPMsM26e.d.mts} +3 -3
  34. package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
  35. package/dist/storage/kysely.cjs +48 -38
  36. package/dist/storage/kysely.cjs.map +1 -1
  37. package/dist/storage/kysely.d.cts +29 -9
  38. package/dist/storage/kysely.d.cts.map +1 -1
  39. package/dist/storage/kysely.d.mts +29 -9
  40. package/dist/storage/kysely.d.mts.map +1 -1
  41. package/dist/storage/kysely.mjs +48 -38
  42. package/dist/storage/kysely.mjs.map +1 -1
  43. package/dist/tracing-PnECEbEu.d.mts.map +1 -1
  44. package/package.json +6 -3
  45. package/CHANGELOG.md +0 -72
  46. package/dist/middleware-4-GrQlOT.mjs.map +0 -1
  47. package/dist/middleware-DqPQpcup.cjs.map +0 -1
  48. package/dist/otel-40udNP5g.mjs.map +0 -1
  49. package/dist/otel-COrpz_GZ.cjs.map +0 -1
  50. package/src/Telescope.ts +0 -561
  51. package/src/__tests__/Telescope.spec.ts +0 -438
  52. package/src/__tests__/pino-transport.spec.ts +0 -115
  53. package/src/__tests__/redact.spec.ts +0 -336
  54. package/src/adapters/__tests__/lambda.spec.ts +0 -318
  55. package/src/adapters/__tests__/middy.spec.ts +0 -741
  56. package/src/adapters/index.ts +0 -22
  57. package/src/adapters/lambda.ts +0 -432
  58. package/src/adapters/middy.ts +0 -402
  59. package/src/adapters/types.ts +0 -181
  60. package/src/core.ts +0 -51
  61. package/src/index.ts +0 -34
  62. package/src/instrumentation/__tests__/core.spec.ts +0 -248
  63. package/src/instrumentation/__tests__/hono.spec.ts +0 -348
  64. package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
  65. package/src/instrumentation/__tests__/otel.spec.ts +0 -291
  66. package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
  67. package/src/instrumentation/core.ts +0 -228
  68. package/src/instrumentation/hono.ts +0 -270
  69. package/src/instrumentation/http.ts +0 -306
  70. package/src/instrumentation/index.ts +0 -90
  71. package/src/instrumentation/middleware.ts +0 -297
  72. package/src/instrumentation/otel.ts +0 -271
  73. package/src/instrumentation/setup.ts +0 -246
  74. package/src/instrumentation/tracing.ts +0 -145
  75. package/src/logger/__tests__/console.spec.ts +0 -366
  76. package/src/logger/__tests__/pino.spec.ts +0 -217
  77. package/src/logger/console.ts +0 -230
  78. package/src/logger/pino.ts +0 -201
  79. package/src/metrics/MetricsAggregator.ts +0 -555
  80. package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
  81. package/src/metrics/index.ts +0 -2
  82. package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
  83. package/src/metrics/storage/MetricsStorage.ts +0 -74
  84. package/src/metrics/storage/index.ts +0 -3
  85. package/src/metrics/storage/types.ts +0 -243
  86. package/src/otlp/__tests__/receiver.spec.ts +0 -367
  87. package/src/otlp/__tests__/transformer.spec.ts +0 -455
  88. package/src/otlp/hono.ts +0 -120
  89. package/src/otlp/index.ts +0 -22
  90. package/src/otlp/receiver.ts +0 -162
  91. package/src/otlp/transformer.ts +0 -367
  92. package/src/otlp/types.ts +0 -364
  93. package/src/redact.ts +0 -116
  94. package/src/server/__tests__/hono.spec.ts +0 -814
  95. package/src/server/hono.ts +0 -442
  96. package/src/storage/__tests__/kysely.spec.ts +0 -715
  97. package/src/storage/__tests__/memory.spec.ts +0 -694
  98. package/src/storage/kysely.ts +0 -599
  99. package/src/storage/memory.ts +0 -204
  100. package/src/types.ts +0 -447
  101. package/src/ui-assets.ts +0 -40
  102. package/tsconfig.json +0 -9
  103. package/ui/CHANGELOG.md +0 -7
  104. package/ui/index.html +0 -12
  105. package/ui/node_modules/.bin/tsc +0 -21
  106. package/ui/node_modules/.bin/tsserver +0 -21
  107. package/ui/node_modules/.bin/vite +0 -21
  108. package/ui/package.json +0 -24
  109. package/ui/src/App.tsx +0 -595
  110. package/ui/src/api.ts +0 -88
  111. package/ui/src/components/ExceptionDetail.tsx +0 -100
  112. package/ui/src/components/FilterBar.tsx +0 -162
  113. package/ui/src/components/LogDetail.tsx +0 -91
  114. package/ui/src/components/RequestDetail.tsx +0 -143
  115. package/ui/src/main.tsx +0 -10
  116. package/ui/src/styles.css +0 -11
  117. package/ui/src/types.ts +0 -63
  118. package/ui/src/vite-env.d.ts +0 -1
  119. package/ui/src/vite-plugin-gkm-config.ts +0 -54
  120. package/ui/tsconfig.json +0 -20
  121. package/ui/tsconfig.tsbuildinfo +0 -1
  122. package/ui/vite.config.ts +0 -13
@@ -1,814 +0,0 @@
1
- import { Hono } from 'hono';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { InMemoryStorage } from '../../storage/memory';
4
- import { Telescope } from '../../Telescope';
5
- import {
6
- createMiddleware,
7
- createUI,
8
- getRequestId,
9
- getTelescopeContext,
10
- setupWebSocket,
11
- } from '../hono';
12
-
13
- describe('Hono Adapter', () => {
14
- let telescope: Telescope;
15
- let storage: InMemoryStorage;
16
-
17
- beforeEach(() => {
18
- storage = new InMemoryStorage();
19
- telescope = new Telescope({ storage });
20
- });
21
-
22
- afterEach(() => {
23
- telescope.destroy();
24
- });
25
-
26
- describe('createMiddleware', () => {
27
- it('should capture GET requests', async () => {
28
- const app = new Hono();
29
- app.use('*', createMiddleware(telescope));
30
- app.get('/api/users', (c) => c.json({ users: [] }));
31
-
32
- const res = await app.request('/api/users');
33
-
34
- expect(res.status).toBe(200);
35
-
36
- const requests = await telescope.getRequests();
37
- expect(requests).toHaveLength(1);
38
- expect(requests[0].method).toBe('GET');
39
- expect(requests[0].path).toBe('/api/users');
40
- expect(requests[0].status).toBe(200);
41
- });
42
-
43
- it('should capture POST requests with body', async () => {
44
- const app = new Hono();
45
- app.use('*', createMiddleware(telescope));
46
- app.post('/api/users', async (c) => {
47
- const body = await c.req.json();
48
- return c.json({ id: '123', ...body }, 201);
49
- });
50
-
51
- const res = await app.request('/api/users', {
52
- method: 'POST',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ name: 'John' }),
55
- });
56
-
57
- expect(res.status).toBe(201);
58
-
59
- const requests = await telescope.getRequests();
60
- expect(requests).toHaveLength(1);
61
- expect(requests[0].method).toBe('POST');
62
- expect(requests[0].body).toEqual({ name: 'John' });
63
- expect(requests[0].status).toBe(201);
64
- });
65
-
66
- it('should capture query parameters', async () => {
67
- const app = new Hono();
68
- app.use('*', createMiddleware(telescope));
69
- app.get('/api/search', (c) => c.json({ query: c.req.query('q') }));
70
-
71
- await app.request('/api/search?q=test&limit=10');
72
-
73
- const requests = await telescope.getRequests();
74
- expect(requests[0].query).toEqual({ q: 'test', limit: '10' });
75
- });
76
-
77
- it('should capture request headers', async () => {
78
- const app = new Hono();
79
- app.use('*', createMiddleware(telescope));
80
- app.get('/api/test', (c) => c.json({ ok: true }));
81
-
82
- await app.request('/api/test', {
83
- headers: {
84
- Authorization: 'Bearer token123',
85
- 'X-Custom-Header': 'custom-value',
86
- },
87
- });
88
-
89
- const requests = await telescope.getRequests();
90
- expect(requests[0].headers.authorization).toBe('Bearer token123');
91
- expect(requests[0].headers['x-custom-header']).toBe('custom-value');
92
- });
93
-
94
- it('should capture response duration', async () => {
95
- const app = new Hono();
96
- app.use('*', createMiddleware(telescope));
97
- app.get('/api/slow', async (c) => {
98
- await new Promise((resolve) => setTimeout(resolve, 50));
99
- return c.json({ ok: true });
100
- });
101
-
102
- await app.request('/api/slow');
103
-
104
- const requests = await telescope.getRequests();
105
- expect(requests[0].duration).toBeGreaterThan(40);
106
- });
107
-
108
- it('should handle errors thrown in handlers', async () => {
109
- const app = new Hono();
110
- app.use('*', createMiddleware(telescope));
111
- app.get('/api/error', () => {
112
- throw new Error('Test error');
113
- });
114
-
115
- // Hono catches errors and returns 500
116
- const res = await app.request('/api/error');
117
- expect(res.status).toBe(500);
118
-
119
- // The error is logged to stderr but the request still completes
120
- // Exception recording depends on Hono's error handling behavior
121
- });
122
-
123
- it('should skip ignored paths', async () => {
124
- const ignoredTelescope = new Telescope({
125
- storage,
126
- ignorePatterns: ['/health', '/__telescope/*'],
127
- });
128
-
129
- const app = new Hono();
130
- app.use('*', createMiddleware(ignoredTelescope));
131
- app.get('/health', (c) => c.json({ status: 'ok' }));
132
- app.get('/api/users', (c) => c.json({ users: [] }));
133
-
134
- await app.request('/health');
135
- await app.request('/api/users');
136
-
137
- const requests = await ignoredTelescope.getRequests();
138
- expect(requests).toHaveLength(1);
139
- expect(requests[0].path).toBe('/api/users');
140
-
141
- ignoredTelescope.destroy();
142
- });
143
-
144
- it('should skip when telescope is disabled', async () => {
145
- const disabled = new Telescope({ storage, enabled: false });
146
-
147
- const app = new Hono();
148
- app.use('*', createMiddleware(disabled));
149
- app.get('/api/test', (c) => c.json({ ok: true }));
150
-
151
- await app.request('/api/test');
152
-
153
- const requests = await storage.getRequests();
154
- expect(requests).toHaveLength(0);
155
-
156
- disabled.destroy();
157
- });
158
-
159
- it('should not record body when recordBody is false', async () => {
160
- const noBody = new Telescope({ storage, recordBody: false });
161
-
162
- const app = new Hono();
163
- app.use('*', createMiddleware(noBody));
164
- app.post('/api/users', async (c) => {
165
- await c.req.json();
166
- return c.json({ id: '123' });
167
- });
168
-
169
- await app.request('/api/users', {
170
- method: 'POST',
171
- headers: { 'Content-Type': 'application/json' },
172
- body: JSON.stringify({ name: 'John' }),
173
- });
174
-
175
- const requests = await noBody.getRequests();
176
- expect(requests[0].body).toBeUndefined();
177
-
178
- noBody.destroy();
179
- });
180
-
181
- it('should capture request size from Content-Length header', async () => {
182
- const app = new Hono();
183
- app.use('*', createMiddleware(telescope));
184
- app.post('/api/data', async (c) => {
185
- await c.req.json();
186
- return c.json({ ok: true });
187
- });
188
-
189
- const body = JSON.stringify({ name: 'John', data: 'test' });
190
- await app.request('/api/data', {
191
- method: 'POST',
192
- headers: {
193
- 'Content-Type': 'application/json',
194
- 'Content-Length': String(Buffer.byteLength(body)),
195
- },
196
- body,
197
- });
198
-
199
- const requests = await telescope.getRequests();
200
- expect(requests[0].requestSize).toBe(Buffer.byteLength(body));
201
- });
202
-
203
- it('should capture response size from body when Content-Length not set', async () => {
204
- const app = new Hono();
205
- app.use('*', createMiddleware(telescope));
206
- app.get('/api/data', (c) =>
207
- c.json({ users: ['alice', 'bob', 'charlie'] }),
208
- );
209
-
210
- await app.request('/api/data');
211
-
212
- const requests = await telescope.getRequests();
213
- expect(requests[0].responseSize).toBeGreaterThan(0);
214
- // The response body is '{"users":["alice","bob","charlie"]}'
215
- expect(requests[0].responseSize).toBe(
216
- Buffer.byteLength(
217
- JSON.stringify({ users: ['alice', 'bob', 'charlie'] }),
218
- ),
219
- );
220
- });
221
-
222
- it('should handle multi-byte characters in size calculation', async () => {
223
- const app = new Hono();
224
- app.use('*', createMiddleware(telescope));
225
- app.get('/api/unicode', (c) => c.json({ message: '你好世界' }));
226
-
227
- await app.request('/api/unicode');
228
-
229
- const requests = await telescope.getRequests();
230
- // UTF-8 bytes for Chinese characters are more than string length
231
- const expectedBody = JSON.stringify({ message: '你好世界' });
232
- expect(requests[0].responseSize).toBe(
233
- Buffer.byteLength(expectedBody, 'utf8'),
234
- );
235
- });
236
- });
237
-
238
- describe('createUI', () => {
239
- it('should return requests list', async () => {
240
- // Add a request first
241
- await telescope.recordRequest({
242
- method: 'GET',
243
- path: '/api/test',
244
- url: 'http://localhost/api/test',
245
- headers: {},
246
- query: {},
247
- status: 200,
248
- responseHeaders: {},
249
- duration: 10,
250
- });
251
-
252
- const ui = createUI(telescope);
253
- const res = await ui.request('/api/requests');
254
- const data = await res.json();
255
-
256
- expect(Array.isArray(data)).toBe(true);
257
- expect(data).toHaveLength(1);
258
- });
259
-
260
- it('should return single request by ID', async () => {
261
- const requestId = await telescope.recordRequest({
262
- method: 'GET',
263
- path: '/api/test',
264
- url: 'http://localhost/api/test',
265
- headers: {},
266
- query: {},
267
- status: 200,
268
- responseHeaders: {},
269
- duration: 10,
270
- });
271
-
272
- const ui = createUI(telescope);
273
- const res = await ui.request(`/api/requests/${requestId}`);
274
- const data = await res.json();
275
-
276
- expect(data.id).toBe(requestId);
277
- expect(data.path).toBe('/api/test');
278
- });
279
-
280
- it('should return 404 for non-existent request', async () => {
281
- const ui = createUI(telescope);
282
- const res = await ui.request('/api/requests/non-existent');
283
-
284
- expect(res.status).toBe(404);
285
- });
286
-
287
- it('should return exceptions list', async () => {
288
- await telescope.exception(new Error('Test error'));
289
-
290
- const ui = createUI(telescope);
291
- const res = await ui.request('/api/exceptions');
292
- const data = await res.json();
293
-
294
- expect(Array.isArray(data)).toBe(true);
295
- expect(data).toHaveLength(1);
296
- expect(data[0].message).toBe('Test error');
297
- });
298
-
299
- it('should return logs list', async () => {
300
- await telescope.info('Test log', { key: 'value' });
301
-
302
- const ui = createUI(telescope);
303
- const res = await ui.request('/api/logs');
304
- const data = await res.json();
305
-
306
- expect(Array.isArray(data)).toBe(true);
307
- expect(data).toHaveLength(1);
308
- expect(data[0].message).toBe('Test log');
309
- });
310
-
311
- it('should return stats', async () => {
312
- await telescope.recordRequest({
313
- method: 'GET',
314
- path: '/test',
315
- url: 'http://localhost/test',
316
- headers: {},
317
- query: {},
318
- status: 200,
319
- responseHeaders: {},
320
- duration: 10,
321
- });
322
- await telescope.info('Test');
323
- await telescope.exception(new Error('Test'));
324
-
325
- const ui = createUI(telescope);
326
- const res = await ui.request('/api/stats');
327
- const data = await res.json();
328
-
329
- expect(data.requests).toBe(1);
330
- expect(data.logs).toBe(1);
331
- expect(data.exceptions).toBe(1);
332
- });
333
-
334
- it('should return dashboard HTML on root', async () => {
335
- const ui = createUI(telescope);
336
- const res = await ui.request('/');
337
-
338
- expect(res.headers.get('content-type')).toContain('text/html');
339
- const html = await res.text();
340
- expect(html).toContain('<!DOCTYPE html>');
341
- });
342
-
343
- it('should support pagination query params', async () => {
344
- for (let i = 0; i < 10; i++) {
345
- await telescope.recordRequest({
346
- method: 'GET',
347
- path: `/api/item/${i}`,
348
- url: `http://localhost/api/item/${i}`,
349
- headers: {},
350
- query: {},
351
- status: 200,
352
- responseHeaders: {},
353
- duration: 10,
354
- });
355
- }
356
-
357
- const ui = createUI(telescope);
358
- const res = await ui.request('/api/requests?limit=3&offset=0');
359
- const data = await res.json();
360
-
361
- expect(data).toHaveLength(3);
362
- });
363
- });
364
-
365
- describe('getRequestId', () => {
366
- it('should return undefined during handler (ID is set after response)', async () => {
367
- let capturedRequestId: string | undefined;
368
-
369
- const app = new Hono();
370
- app.use('*', createMiddleware(telescope));
371
- app.get('/api/test', (c) => {
372
- // Note: The request ID is set AFTER next() completes,
373
- // so it's not available during the handler execution
374
- capturedRequestId = getRequestId(c);
375
- return c.json({ ok: true });
376
- });
377
-
378
- await app.request('/api/test');
379
-
380
- // The ID is not available during handler execution
381
- expect(capturedRequestId).toBeUndefined();
382
-
383
- // But the request was still recorded
384
- const requests = await telescope.getRequests();
385
- expect(requests).toHaveLength(1);
386
- });
387
-
388
- it('should return undefined when middleware not used', async () => {
389
- let capturedRequestId: string | undefined;
390
-
391
- const app = new Hono();
392
- app.get('/api/test', (c) => {
393
- capturedRequestId = getRequestId(c);
394
- return c.json({ ok: true });
395
- });
396
-
397
- await app.request('/api/test');
398
-
399
- expect(capturedRequestId).toBeUndefined();
400
- });
401
- });
402
-
403
- describe('getTelescopeContext', () => {
404
- it('should return undefined when middleware not used', async () => {
405
- let ctx: ReturnType<typeof getTelescopeContext>;
406
-
407
- const app = new Hono();
408
- app.get('/api/test', (c) => {
409
- ctx = getTelescopeContext(c);
410
- return c.json({ ok: true });
411
- });
412
-
413
- await app.request('/api/test');
414
-
415
- expect(ctx).toBeUndefined();
416
- });
417
- });
418
-
419
- describe('createMiddleware - edge cases', () => {
420
- it('should capture IP from x-forwarded-for header', async () => {
421
- const app = new Hono();
422
- app.use('*', createMiddleware(telescope));
423
- app.get('/api/test', (c) => c.json({ ok: true }));
424
-
425
- await app.request('/api/test', {
426
- headers: { 'x-forwarded-for': '192.168.1.1' },
427
- });
428
-
429
- const requests = await telescope.getRequests();
430
- expect(requests[0].ip).toBe('192.168.1.1');
431
- });
432
-
433
- it('should capture IP from x-real-ip header', async () => {
434
- const app = new Hono();
435
- app.use('*', createMiddleware(telescope));
436
- app.get('/api/test', (c) => c.json({ ok: true }));
437
-
438
- await app.request('/api/test', {
439
- headers: { 'x-real-ip': '10.0.0.1' },
440
- });
441
-
442
- const requests = await telescope.getRequests();
443
- expect(requests[0].ip).toBe('10.0.0.1');
444
- });
445
-
446
- it('should capture form-urlencoded body', async () => {
447
- const app = new Hono();
448
- app.use('*', createMiddleware(telescope));
449
- app.post('/api/form', async (c) => {
450
- const body = await c.req.formData();
451
- return c.json({ name: body.get('name') });
452
- });
453
-
454
- const formData = new FormData();
455
- formData.append('name', 'John');
456
-
457
- await app.request('/api/form', {
458
- method: 'POST',
459
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
460
- body: new URLSearchParams({ name: 'John' }),
461
- });
462
-
463
- const requests = await telescope.getRequests();
464
- expect(requests[0].method).toBe('POST');
465
- });
466
-
467
- it('should capture text body', async () => {
468
- const app = new Hono();
469
- app.use('*', createMiddleware(telescope));
470
- app.post('/api/text', async (c) => {
471
- const body = await c.req.text();
472
- return c.text(`Received: ${body}`);
473
- });
474
-
475
- await app.request('/api/text', {
476
- method: 'POST',
477
- headers: { 'Content-Type': 'text/plain' },
478
- body: 'Hello, World!',
479
- });
480
-
481
- const requests = await telescope.getRequests();
482
- expect(requests[0].body).toBe('Hello, World!');
483
- });
484
-
485
- it('should handle PUT requests', async () => {
486
- const app = new Hono();
487
- app.use('*', createMiddleware(telescope));
488
- app.put('/api/users/:id', async (c) => {
489
- const body = await c.req.json();
490
- return c.json({ id: c.req.param('id'), ...body });
491
- });
492
-
493
- await app.request('/api/users/123', {
494
- method: 'PUT',
495
- headers: { 'Content-Type': 'application/json' },
496
- body: JSON.stringify({ name: 'Jane' }),
497
- });
498
-
499
- const requests = await telescope.getRequests();
500
- expect(requests[0].body).toEqual({ name: 'Jane' });
501
- });
502
-
503
- it('should handle PATCH requests', async () => {
504
- const app = new Hono();
505
- app.use('*', createMiddleware(telescope));
506
- app.patch('/api/users/:id', async (c) => {
507
- const body = await c.req.json();
508
- return c.json({ updated: true, ...body });
509
- });
510
-
511
- await app.request('/api/users/123', {
512
- method: 'PATCH',
513
- headers: { 'Content-Type': 'application/json' },
514
- body: JSON.stringify({ name: 'Updated' }),
515
- });
516
-
517
- const requests = await telescope.getRequests();
518
- expect(requests[0].body).toEqual({ name: 'Updated' });
519
- });
520
-
521
- it('should handle body parsing errors gracefully', async () => {
522
- const app = new Hono();
523
- app.use('*', createMiddleware(telescope));
524
- app.post('/api/test', (c) => c.json({ ok: true }));
525
-
526
- await app.request('/api/test', {
527
- method: 'POST',
528
- headers: { 'Content-Type': 'application/json' },
529
- body: 'invalid json{{{',
530
- });
531
-
532
- const requests = await telescope.getRequests();
533
- // Request should still be recorded even with body parsing error
534
- expect(requests).toHaveLength(1);
535
- });
536
- });
537
-
538
- describe('createUI - metrics endpoints', () => {
539
- it('should return metrics', async () => {
540
- await telescope.recordRequest({
541
- method: 'GET',
542
- path: '/api/test',
543
- url: 'http://localhost/api/test',
544
- headers: {},
545
- query: {},
546
- status: 200,
547
- responseHeaders: {},
548
- duration: 100,
549
- });
550
-
551
- const ui = createUI(telescope);
552
- const res = await ui.request('/api/metrics');
553
- const data = await res.json();
554
-
555
- expect(data).toHaveProperty('totalRequests');
556
- });
557
-
558
- it('should return endpoint metrics', async () => {
559
- await telescope.recordRequest({
560
- method: 'GET',
561
- path: '/api/users',
562
- url: 'http://localhost/api/users',
563
- headers: {},
564
- query: {},
565
- status: 200,
566
- responseHeaders: {},
567
- duration: 50,
568
- });
569
-
570
- const ui = createUI(telescope);
571
- const res = await ui.request('/api/metrics/endpoints');
572
- const data = await res.json();
573
-
574
- expect(Array.isArray(data)).toBe(true);
575
- });
576
-
577
- it('should return status distribution', async () => {
578
- await telescope.recordRequest({
579
- method: 'GET',
580
- path: '/api/test',
581
- url: 'http://localhost/api/test',
582
- headers: {},
583
- query: {},
584
- status: 200,
585
- responseHeaders: {},
586
- duration: 10,
587
- });
588
-
589
- const ui = createUI(telescope);
590
- const res = await ui.request('/api/metrics/status');
591
- const data = await res.json();
592
-
593
- expect(data).toBeDefined();
594
- });
595
-
596
- it('should return endpoint details', async () => {
597
- await telescope.recordRequest({
598
- method: 'GET',
599
- path: '/api/users',
600
- url: 'http://localhost/api/users',
601
- headers: {},
602
- query: {},
603
- status: 200,
604
- responseHeaders: {},
605
- duration: 50,
606
- });
607
-
608
- const ui = createUI(telescope);
609
- const res = await ui.request(
610
- '/api/metrics/endpoint?method=GET&path=/api/users',
611
- );
612
-
613
- // The endpoint may not exist depending on how metrics are stored
614
- // Just check it returns something valid
615
- expect(res.status === 200 || res.status === 404).toBe(true);
616
- });
617
-
618
- it('should return 400 for endpoint details without method/path', async () => {
619
- const ui = createUI(telescope);
620
- const res = await ui.request('/api/metrics/endpoint');
621
-
622
- expect(res.status).toBe(400);
623
- const data = await res.json();
624
- expect(data.error).toBe('method and path are required');
625
- });
626
-
627
- it('should reset metrics', async () => {
628
- await telescope.recordRequest({
629
- method: 'GET',
630
- path: '/api/test',
631
- url: 'http://localhost/api/test',
632
- headers: {},
633
- query: {},
634
- status: 200,
635
- responseHeaders: {},
636
- duration: 10,
637
- });
638
-
639
- const ui = createUI(telescope);
640
- const res = await ui.request('/api/metrics', { method: 'DELETE' });
641
-
642
- expect(res.status).toBe(200);
643
- const data = await res.json();
644
- expect(data.success).toBe(true);
645
- });
646
- });
647
-
648
- describe('createUI - exception by ID', () => {
649
- it('should return single exception by ID', async () => {
650
- await telescope.exception(new Error('Test exception'));
651
-
652
- const exceptions = await telescope.getExceptions();
653
- const exceptionId = exceptions[0].id;
654
-
655
- const ui = createUI(telescope);
656
- const res = await ui.request(`/api/exceptions/${exceptionId}`);
657
- const data = await res.json();
658
-
659
- expect(data.id).toBe(exceptionId);
660
- expect(data.message).toBe('Test exception');
661
- });
662
-
663
- it('should return 404 for non-existent exception', async () => {
664
- const ui = createUI(telescope);
665
- const res = await ui.request('/api/exceptions/non-existent');
666
-
667
- expect(res.status).toBe(404);
668
- });
669
- });
670
-
671
- describe('createUI - SPA fallback', () => {
672
- it('should serve HTML for SPA routes', async () => {
673
- const ui = createUI(telescope);
674
- const res = await ui.request('/requests/some-id');
675
-
676
- expect(res.headers.get('content-type')).toContain('text/html');
677
- });
678
- });
679
-
680
- describe('createUI - query parsing', () => {
681
- it('should parse search query', async () => {
682
- await telescope.recordRequest({
683
- method: 'GET',
684
- path: '/api/users',
685
- url: 'http://localhost/api/users',
686
- headers: {},
687
- query: {},
688
- status: 200,
689
- responseHeaders: {},
690
- duration: 10,
691
- });
692
-
693
- const ui = createUI(telescope);
694
- const res = await ui.request('/api/requests?search=users');
695
- const data = await res.json();
696
-
697
- expect(Array.isArray(data)).toBe(true);
698
- });
699
-
700
- it('should parse date range queries', async () => {
701
- const ui = createUI(telescope);
702
- const res = await ui.request(
703
- '/api/requests?before=2024-01-01&after=2023-01-01',
704
- );
705
- const data = await res.json();
706
-
707
- expect(Array.isArray(data)).toBe(true);
708
- });
709
-
710
- it('should parse tags query', async () => {
711
- const ui = createUI(telescope);
712
- const res = await ui.request('/api/requests?tags=api,users');
713
- const data = await res.json();
714
-
715
- expect(Array.isArray(data)).toBe(true);
716
- });
717
-
718
- it('should parse method filter', async () => {
719
- await telescope.recordRequest({
720
- method: 'GET',
721
- path: '/api/test',
722
- url: 'http://localhost/api/test',
723
- headers: {},
724
- query: {},
725
- status: 200,
726
- responseHeaders: {},
727
- duration: 10,
728
- });
729
-
730
- const ui = createUI(telescope);
731
- const res = await ui.request('/api/requests?method=GET');
732
- const data = await res.json();
733
-
734
- expect(Array.isArray(data)).toBe(true);
735
- });
736
-
737
- it('should parse status filter', async () => {
738
- const ui = createUI(telescope);
739
- const res = await ui.request('/api/requests?status=200');
740
- const data = await res.json();
741
-
742
- expect(Array.isArray(data)).toBe(true);
743
- });
744
-
745
- it('should parse log level filter', async () => {
746
- await telescope.info('Test info');
747
- await telescope.error('Test error');
748
-
749
- const ui = createUI(telescope);
750
- const res = await ui.request('/api/logs?level=error');
751
- const data = await res.json();
752
-
753
- expect(Array.isArray(data)).toBe(true);
754
- });
755
-
756
- it('should parse metrics query options', async () => {
757
- const ui = createUI(telescope);
758
- const res = await ui.request(
759
- '/api/metrics?start=2024-01-01&end=2024-12-31&bucketSize=3600000&limit=10',
760
- );
761
- const data = await res.json();
762
-
763
- expect(data).toBeDefined();
764
- });
765
- });
766
-
767
- describe('setupWebSocket', () => {
768
- it('should setup WebSocket route with default options', () => {
769
- const app = new Hono();
770
- const mockUpgrade = vi.fn(() => vi.fn());
771
-
772
- setupWebSocket(app, telescope, mockUpgrade);
773
-
774
- expect(mockUpgrade).toHaveBeenCalled();
775
- });
776
-
777
- it('should setup WebSocket route with custom options', () => {
778
- const app = new Hono();
779
- const mockUpgrade = vi.fn(() => vi.fn());
780
-
781
- setupWebSocket(app, telescope, mockUpgrade, {
782
- broadcastMetrics: false,
783
- metricsBroadcastInterval: 10000,
784
- });
785
-
786
- expect(mockUpgrade).toHaveBeenCalled();
787
- });
788
-
789
- it('should start metrics broadcast when enabled', () => {
790
- const app = new Hono();
791
- const mockUpgrade = vi.fn(() => vi.fn());
792
- const startSpy = vi.spyOn(telescope, 'startMetricsBroadcast');
793
-
794
- setupWebSocket(app, telescope, mockUpgrade, {
795
- broadcastMetrics: true,
796
- metricsBroadcastInterval: 5000,
797
- });
798
-
799
- expect(startSpy).toHaveBeenCalledWith(5000);
800
- });
801
-
802
- it('should not start metrics broadcast when disabled', () => {
803
- const app = new Hono();
804
- const mockUpgrade = vi.fn(() => vi.fn());
805
- const startSpy = vi.spyOn(telescope, 'startMetricsBroadcast');
806
-
807
- setupWebSocket(app, telescope, mockUpgrade, {
808
- broadcastMetrics: false,
809
- });
810
-
811
- expect(startSpy).not.toHaveBeenCalled();
812
- });
813
- });
814
- });