adonisjs-server-stats 1.1.4 → 1.2.2

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
@@ -35,6 +35,7 @@ Zero frontend dependencies. Zero build step. Just `@serverStats()` and go.
35
35
 
36
36
  - **Live stats bar** -- CPU, memory, event loop lag, HTTP throughput, DB pool, Redis, queues, logs
37
37
  - **Debug toolbar** -- SQL queries, events, emails, routes, logs with search and filtering
38
+ - **Request tracing** -- per-request waterfall timeline showing DB queries, events, and custom spans
38
39
  - **Custom panes** -- add your own tabs (webhooks, emails, cache, anything) with a simple config
39
40
  - **Pluggable collectors** -- use built-in collectors or write your own
40
41
  - **Visibility control** -- show only to admins, specific roles, or in dev mode
@@ -223,6 +224,8 @@ export default class ServerStatsController {
223
224
  | `maxEmails` | `number` | `100` | Max emails to buffer |
224
225
  | `slowQueryThresholdMs` | `number` | `100` | Slow query threshold (ms) |
225
226
  | `persistDebugData` | `boolean \| string` | `false` | Persist debug data to disk across restarts. `true` writes to `.adonisjs/server-stats/debug-data.json`, or pass a custom path. |
227
+ | `tracing` | `boolean` | `false` | Enable per-request tracing with timeline visualization |
228
+ | `maxTraces` | `number` | `200` | Max request traces to buffer |
226
229
  | `panes` | `DebugPane[]` | -- | Custom debug panel tabs |
227
230
 
228
231
  ---
@@ -372,7 +375,7 @@ Features:
372
375
 
373
376
  ## Dev Toolbar
374
377
 
375
- Adds a debug panel with SQL query inspection, event tracking, email capture with HTML preview, route table, and live logs. Only active in non-production environments.
378
+ Adds a debug panel with SQL query inspection, event tracking, email capture with HTML preview, route table, live logs, and per-request tracing. Only active in non-production environments.
376
379
 
377
380
  ```ts
378
381
  export default defineConfig({
@@ -383,6 +386,7 @@ export default defineConfig({
383
386
  maxEmails: 100,
384
387
  slowQueryThresholdMs: 100,
385
388
  persistDebugData: true, // or a custom path: 'custom/debug.json'
389
+ tracing: true, // enable per-request timeline
386
390
  },
387
391
  })
388
392
  ```
@@ -398,6 +402,8 @@ router
398
402
  router.get('routes', '#controllers/admin/debug_controller.routes')
399
403
  router.get('emails', '#controllers/admin/debug_controller.emails')
400
404
  router.get('emails/:id/preview', '#controllers/admin/debug_controller.emailPreview')
405
+ router.get('traces', '#controllers/admin/debug_controller.traces')
406
+ router.get('traces/:id', '#controllers/admin/debug_controller.traceDetail')
401
407
  })
402
408
  .prefix('/admin/api/debug')
403
409
  .use(middleware.admin())
@@ -414,6 +420,52 @@ Enable `persistDebugData: true` to save queries, events, and emails to `.adonisj
414
420
  - **Flushed** every 30 seconds (handles crashes)
415
421
  - **Saved** on graceful shutdown
416
422
 
423
+ ### Request Tracing
424
+
425
+ When `tracing: true` is set, the debug panel gains a **Timeline** tab that shows a waterfall view of every HTTP request -- which DB queries ran, in what order, and how long each took.
426
+
427
+ Tracing uses `AsyncLocalStorage` to automatically correlate operations to the request that triggered them. DB queries captured via `db:query` events and `console.warn` calls are automatically attached to the active request trace.
428
+
429
+ #### How it works
430
+
431
+ ```
432
+ GET /organizations/create 286ms
433
+ ├─ SELECT * FROM users 2ms █
434
+ ├─ SELECT * FROM orgs 4ms █
435
+ ├─ fetchMembers (custom) 180ms ██████████████████
436
+ └─ response sent 5ms ██
437
+ ```
438
+
439
+ 1. The **Timeline** tab shows a list of recent requests with method, URL, status code, duration, span count, and any warnings
440
+ 2. Click a request to see the **waterfall chart** -- each span is a horizontal bar positioned by time offset, color-coded by category
441
+ 3. Spans can be nested (a custom span wrapping DB queries will show them indented)
442
+
443
+ #### Span categories
444
+
445
+ | Category | Color | Auto-captured |
446
+ |----------|--------|---------------|
447
+ | DB | Purple | `db:query` events |
448
+ | Request | Blue | Full request lifecycle |
449
+ | Mail | Green | -- |
450
+ | Event | Amber | -- |
451
+ | View | Cyan | -- |
452
+ | Custom | Gray | Via `trace()` helper |
453
+
454
+ #### Custom spans
455
+
456
+ Use the `trace()` helper to wrap any async code in a named span:
457
+
458
+ ```ts
459
+ import { trace } from 'adonisjs-server-stats'
460
+
461
+ // In a controller or service:
462
+ const result = await trace('organization.fetchMembers', async () => {
463
+ return OrganizationService.getMembers(orgId)
464
+ })
465
+ ```
466
+
467
+ If tracing is disabled or no request is active, `trace()` executes the function directly with no overhead.
468
+
417
469
  ### Custom Debug Panes
418
470
 
419
471
  Add custom tabs to the debug panel:
@@ -573,8 +625,13 @@ import type {
573
625
  EventRecord,
574
626
  EmailRecord,
575
627
  RouteRecord,
628
+ TraceSpan,
629
+ TraceRecord,
576
630
  } from 'adonisjs-server-stats'
577
631
 
632
+ // Trace helper
633
+ import { trace } from 'adonisjs-server-stats'
634
+
578
635
  // Collector option types
579
636
  import type {
580
637
  HttpCollectorOptions,
@@ -8,5 +8,7 @@ export default class DebugController {
8
8
  routes({ response }: HttpContext): Promise<void>;
9
9
  emails({ response }: HttpContext): Promise<void>;
10
10
  emailPreview({ params, response }: HttpContext): Promise<void>;
11
+ traces({ response }: HttpContext): Promise<void>;
12
+ traceDetail({ params, response }: HttpContext): Promise<void>;
11
13
  }
12
14
  //# sourceMappingURL=debug_controller.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,OAAO,OAAO,eAAe;IACtB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE/B,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAMjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAOhC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;CAQrD"}
1
+ {"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,OAAO,OAAO,eAAe;IACtB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE/B,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAMjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAOhC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;IAS9C,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAahC,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;CAWpD"}
@@ -30,4 +30,27 @@ export default class DebugController {
30
30
  }
31
31
  return response.header('Content-Type', 'text/html; charset=utf-8').send(html);
32
32
  }
33
+ async traces({ response }) {
34
+ if (!this.store.traces) {
35
+ return response.json({ traces: [], total: 0 });
36
+ }
37
+ const traces = this.store.traces.getLatest(100);
38
+ // Strip spans from list view to keep it lightweight
39
+ const list = traces.map(({ spans, warnings, ...rest }) => ({
40
+ ...rest,
41
+ warningCount: warnings.length,
42
+ }));
43
+ return response.json({ traces: list, total: this.store.traces.getTotalCount() });
44
+ }
45
+ async traceDetail({ params, response }) {
46
+ if (!this.store.traces) {
47
+ return response.notFound({ error: 'Tracing not enabled' });
48
+ }
49
+ const id = Number(params.id);
50
+ const trace = this.store.traces.getTrace(id);
51
+ if (!trace) {
52
+ return response.notFound({ error: 'Trace not found' });
53
+ }
54
+ return response.json(trace);
55
+ }
33
56
  }
@@ -2,6 +2,7 @@ import { QueryCollector } from "./query_collector.js";
2
2
  import { EventCollector } from "./event_collector.js";
3
3
  import { EmailCollector } from "./email_collector.js";
4
4
  import { RouteInspector } from "./route_inspector.js";
5
+ import { TraceCollector } from "./trace_collector.js";
5
6
  import type { DevToolbarConfig } from "./types.js";
6
7
  /**
7
8
  * Singleton store holding all debug data collectors.
@@ -12,6 +13,7 @@ export declare class DebugStore {
12
13
  readonly events: EventCollector;
13
14
  readonly emails: EmailCollector;
14
15
  readonly routes: RouteInspector;
16
+ readonly traces: TraceCollector | null;
15
17
  constructor(config: DevToolbarConfig);
16
18
  start(emitter: any, router: any): Promise<void>;
17
19
  stop(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;gBAEpB,MAAM,EAAE,gBAAgB;IAU9B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD,IAAI,IAAI,IAAI;IAMZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAajD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBpD"}
1
+ {"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;gBAE3B,MAAM,EAAE,gBAAgB;IAW9B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAQrD,IAAI,IAAI,IAAI;IAOZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAuBpD"}
@@ -4,6 +4,7 @@ import { QueryCollector } from "./query_collector.js";
4
4
  import { EventCollector } from "./event_collector.js";
5
5
  import { EmailCollector } from "./email_collector.js";
6
6
  import { RouteInspector } from "./route_inspector.js";
7
+ import { TraceCollector } from "./trace_collector.js";
7
8
  /**
8
9
  * Singleton store holding all debug data collectors.
9
10
  * Bound to the AdonisJS container as `debug.store`.
@@ -13,22 +14,26 @@ export class DebugStore {
13
14
  events;
14
15
  emails;
15
16
  routes;
17
+ traces;
16
18
  constructor(config) {
17
19
  this.queries = new QueryCollector(config.maxQueries, config.slowQueryThresholdMs);
18
20
  this.events = new EventCollector(config.maxEvents);
19
21
  this.emails = new EmailCollector(config.maxEmails);
20
22
  this.routes = new RouteInspector();
23
+ this.traces = config.tracing ? new TraceCollector(config.maxTraces) : null;
21
24
  }
22
25
  async start(emitter, router) {
23
26
  await this.queries.start(emitter);
24
27
  this.events.start(emitter);
25
28
  await this.emails.start(emitter);
26
29
  this.routes.inspect(router);
30
+ this.traces?.start(emitter);
27
31
  }
28
32
  stop() {
29
33
  this.queries.stop();
30
34
  this.events.stop();
31
35
  this.emails.stop();
36
+ this.traces?.stop();
32
37
  }
33
38
  /** Serialize all collector data to a JSON file (atomic write). */
34
39
  async saveToDisk(filePath) {
@@ -37,6 +42,9 @@ export class DebugStore {
37
42
  events: this.events.getEvents(),
38
43
  emails: this.emails.getEmails(),
39
44
  };
45
+ if (this.traces) {
46
+ data.traces = this.traces.getTraces();
47
+ }
40
48
  const json = JSON.stringify(data);
41
49
  const tmpPath = filePath + ".tmp";
42
50
  await mkdir(dirname(filePath), { recursive: true });
@@ -62,5 +70,8 @@ export class DebugStore {
62
70
  if (Array.isArray(data.emails) && data.emails.length > 0) {
63
71
  this.emails.loadRecords(data.emails);
64
72
  }
73
+ if (this.traces && Array.isArray(data.traces) && data.traces.length > 0) {
74
+ this.traces.loadRecords(data.traces);
75
+ }
65
76
  }
66
77
  }
@@ -0,0 +1,51 @@
1
+ import type { TraceSpan, TraceRecord } from './types.js';
2
+ /**
3
+ * Wrap an async function in a traced span.
4
+ *
5
+ * If tracing is not enabled or no request is active, the function
6
+ * is executed directly without overhead.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { trace } from 'adonisjs-server-stats'
11
+ *
12
+ * const result = await trace('fetchMembers', async () => {
13
+ * return OrganizationService.getMembers(orgId)
14
+ * })
15
+ * ```
16
+ */
17
+ export declare function trace<T>(label: string, fn: () => Promise<T>): Promise<T>;
18
+ /**
19
+ * Collects per-request traces using AsyncLocalStorage.
20
+ *
21
+ * Automatically captures DB queries and console.warn calls within
22
+ * the request context. Users can add custom spans via {@link trace}.
23
+ */
24
+ export declare class TraceCollector {
25
+ private buffer;
26
+ private als;
27
+ private emitter;
28
+ private dbHandler;
29
+ private originalConsoleWarn;
30
+ constructor(maxTraces?: number);
31
+ /** Start a new trace context for an HTTP request. */
32
+ startTrace(callback: () => Promise<void>): Promise<void>;
33
+ /** Finish the current trace and save it to the ring buffer. */
34
+ finishTrace(method: string, url: string, statusCode: number): void;
35
+ /** Add a span to the current trace (if active). */
36
+ addSpan(label: string, category: TraceSpan['category'], startOffset: number, duration: number, metadata?: Record<string, any>): void;
37
+ /** Wrap a function in a traced span with automatic nesting. */
38
+ span<T>(label: string, category: TraceSpan['category'], fn: () => Promise<T>): Promise<T>;
39
+ /** Hook into db:query events and console.warn to auto-create spans. */
40
+ start(emitter: any): void;
41
+ /** Unhook event listeners and restore console.warn. */
42
+ stop(): void;
43
+ getTraces(): TraceRecord[];
44
+ getLatest(n: number): TraceRecord[];
45
+ getTrace(id: number): TraceRecord | undefined;
46
+ getTotalCount(): number;
47
+ clear(): void;
48
+ /** Restore persisted records into the buffer and reset the ID counter. */
49
+ loadRecords(records: TraceRecord[]): void;
50
+ }
51
+ //# sourceMappingURL=trace_collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/trace_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAmBxD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAG9E;AAED;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,GAAG,CAAwC;IACnD,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,mBAAmB,CAAmC;gBAElD,SAAS,GAAE,MAAY;IAKnC,qDAAqD;IACrD,UAAU,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxD,+DAA+D;IAC/D,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAqBlE,mDAAmD;IACnD,OAAO,CACL,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC7B,IAAI;IAeP,+DAA+D;IACzD,IAAI,CAAC,CAAC,EACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC;IAyBb,uEAAuE;IACvE,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IAsCzB,uDAAuD;IACvD,IAAI,IAAI,IAAI;IAaZ,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE;IAInC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI7C,aAAa,IAAI,MAAM;IAIvB,KAAK,IAAI,IAAI;IAIb,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
@@ -0,0 +1,181 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { RingBuffer } from './ring_buffer.js';
4
+ /**
5
+ * Module-level singleton reference for the `trace()` helper.
6
+ */
7
+ let globalTraceCollector = null;
8
+ /**
9
+ * Wrap an async function in a traced span.
10
+ *
11
+ * If tracing is not enabled or no request is active, the function
12
+ * is executed directly without overhead.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { trace } from 'adonisjs-server-stats'
17
+ *
18
+ * const result = await trace('fetchMembers', async () => {
19
+ * return OrganizationService.getMembers(orgId)
20
+ * })
21
+ * ```
22
+ */
23
+ export async function trace(label, fn) {
24
+ if (!globalTraceCollector)
25
+ return fn();
26
+ return globalTraceCollector.span(label, 'custom', fn);
27
+ }
28
+ /**
29
+ * Collects per-request traces using AsyncLocalStorage.
30
+ *
31
+ * Automatically captures DB queries and console.warn calls within
32
+ * the request context. Users can add custom spans via {@link trace}.
33
+ */
34
+ export class TraceCollector {
35
+ buffer;
36
+ als = new AsyncLocalStorage();
37
+ emitter = null;
38
+ dbHandler = null;
39
+ originalConsoleWarn = null;
40
+ constructor(maxTraces = 200) {
41
+ this.buffer = new RingBuffer(maxTraces);
42
+ globalTraceCollector = this;
43
+ }
44
+ /** Start a new trace context for an HTTP request. */
45
+ startTrace(callback) {
46
+ const ctx = {
47
+ requestStart: performance.now(),
48
+ spans: [],
49
+ warnings: [],
50
+ nextSpanId: 1,
51
+ currentSpanId: null,
52
+ };
53
+ return this.als.run(ctx, callback);
54
+ }
55
+ /** Finish the current trace and save it to the ring buffer. */
56
+ finishTrace(method, url, statusCode) {
57
+ const ctx = this.als.getStore();
58
+ if (!ctx)
59
+ return;
60
+ const totalDuration = performance.now() - ctx.requestStart;
61
+ const record = {
62
+ id: this.buffer.getNextId(),
63
+ method,
64
+ url,
65
+ statusCode,
66
+ totalDuration: Math.round(totalDuration * 100) / 100,
67
+ spanCount: ctx.spans.length,
68
+ spans: ctx.spans,
69
+ warnings: ctx.warnings,
70
+ timestamp: Date.now(),
71
+ };
72
+ this.buffer.push(record);
73
+ }
74
+ /** Add a span to the current trace (if active). */
75
+ addSpan(label, category, startOffset, duration, metadata) {
76
+ const ctx = this.als.getStore();
77
+ if (!ctx)
78
+ return;
79
+ ctx.spans.push({
80
+ id: String(ctx.nextSpanId++),
81
+ parentId: ctx.currentSpanId,
82
+ label,
83
+ category,
84
+ startOffset: Math.round(startOffset * 100) / 100,
85
+ duration: Math.round(duration * 100) / 100,
86
+ metadata,
87
+ });
88
+ }
89
+ /** Wrap a function in a traced span with automatic nesting. */
90
+ async span(label, category, fn) {
91
+ const ctx = this.als.getStore();
92
+ if (!ctx)
93
+ return fn();
94
+ const start = performance.now();
95
+ const parentId = ctx.currentSpanId;
96
+ const spanId = String(ctx.nextSpanId++);
97
+ ctx.currentSpanId = spanId;
98
+ try {
99
+ return await fn();
100
+ }
101
+ finally {
102
+ const duration = performance.now() - start;
103
+ ctx.spans.push({
104
+ id: spanId,
105
+ parentId,
106
+ label,
107
+ category,
108
+ startOffset: Math.round((start - ctx.requestStart) * 100) / 100,
109
+ duration: Math.round(duration * 100) / 100,
110
+ });
111
+ ctx.currentSpanId = parentId;
112
+ }
113
+ }
114
+ /** Hook into db:query events and console.warn to auto-create spans. */
115
+ start(emitter) {
116
+ this.emitter = emitter;
117
+ if (emitter && typeof emitter.on === 'function') {
118
+ this.dbHandler = (data) => {
119
+ const ctx = this.als.getStore();
120
+ if (!ctx)
121
+ return;
122
+ const duration = typeof data.duration === 'number'
123
+ ? data.duration
124
+ : Array.isArray(data.duration)
125
+ ? data.duration[0] * 1e3 + data.duration[1] / 1e6
126
+ : 0;
127
+ const offset = performance.now() - ctx.requestStart - duration;
128
+ this.addSpan(data.sql || 'query', 'db', offset, duration, {
129
+ method: data.method,
130
+ model: data.model,
131
+ connection: data.connection,
132
+ });
133
+ };
134
+ emitter.on('db:query', this.dbHandler);
135
+ }
136
+ // Intercept console.warn to capture warnings per-request
137
+ this.originalConsoleWarn = console.warn;
138
+ const self = this;
139
+ console.warn = function (...args) {
140
+ const ctx = self.als.getStore();
141
+ if (ctx) {
142
+ ctx.warnings.push(args.map(String).join(' '));
143
+ }
144
+ self.originalConsoleWarn.apply(console, args);
145
+ };
146
+ }
147
+ /** Unhook event listeners and restore console.warn. */
148
+ stop() {
149
+ if (this.emitter && this.dbHandler) {
150
+ this.emitter.off('db:query', this.dbHandler);
151
+ }
152
+ if (this.originalConsoleWarn) {
153
+ console.warn = this.originalConsoleWarn;
154
+ }
155
+ this.dbHandler = null;
156
+ this.emitter = null;
157
+ this.originalConsoleWarn = null;
158
+ globalTraceCollector = null;
159
+ }
160
+ getTraces() {
161
+ return this.buffer.toArray();
162
+ }
163
+ getLatest(n) {
164
+ return this.buffer.latest(n);
165
+ }
166
+ getTrace(id) {
167
+ return this.buffer.toArray().find((t) => t.id === id);
168
+ }
169
+ getTotalCount() {
170
+ return this.buffer.size();
171
+ }
172
+ clear() {
173
+ this.buffer.clear();
174
+ }
175
+ /** Restore persisted records into the buffer and reset the ID counter. */
176
+ loadRecords(records) {
177
+ this.buffer.load(records);
178
+ const maxId = records.reduce((m, r) => Math.max(m, r.id), 0);
179
+ this.buffer.setNextId(maxId + 1);
180
+ }
181
+ }
@@ -96,6 +96,54 @@ export interface RouteRecord {
96
96
  */
97
97
  middleware: string[];
98
98
  }
99
+ /**
100
+ * A single span within a request trace.
101
+ *
102
+ * Represents a timed operation (DB query, middleware, custom code block)
103
+ * that occurred during an HTTP request. Spans can be nested via `parentId`.
104
+ */
105
+ export interface TraceSpan {
106
+ /** Unique span ID within the trace. */
107
+ id: string;
108
+ /** Parent span ID, or `null` for root-level spans. */
109
+ parentId: string | null;
110
+ /** Human-readable label (e.g. `"SELECT * FROM users"`, `"auth middleware"`). */
111
+ label: string;
112
+ /** Category for color-coding and grouping in the timeline. */
113
+ category: 'request' | 'middleware' | 'db' | 'view' | 'mail' | 'event' | 'custom';
114
+ /** Milliseconds from request start to span start. */
115
+ startOffset: number;
116
+ /** Span duration in milliseconds. */
117
+ duration: number;
118
+ /** Optional metadata (query bindings, status code, etc.). */
119
+ metadata?: Record<string, any>;
120
+ }
121
+ /**
122
+ * A complete trace for a single HTTP request.
123
+ *
124
+ * Contains all spans captured during the request lifecycle,
125
+ * stored in a {@link RingBuffer} by the {@link TraceCollector}.
126
+ */
127
+ export interface TraceRecord {
128
+ /** Auto-incrementing sequence number. */
129
+ id: number;
130
+ /** HTTP method (e.g. `'GET'`, `'POST'`). */
131
+ method: string;
132
+ /** Request URL including query string. */
133
+ url: string;
134
+ /** HTTP response status code. */
135
+ statusCode: number;
136
+ /** Total request duration in milliseconds. */
137
+ totalDuration: number;
138
+ /** Number of spans captured. */
139
+ spanCount: number;
140
+ /** All spans captured during this request. */
141
+ spans: TraceSpan[];
142
+ /** Warnings captured via `console.warn` during this request. */
143
+ warnings: string[];
144
+ /** Unix timestamp in **milliseconds** when the request started. */
145
+ timestamp: number;
146
+ }
99
147
  /**
100
148
  * Resolved dev toolbar configuration with all defaults applied.
101
149
  *
@@ -115,6 +163,10 @@ export interface DevToolbarConfig {
115
163
  slowQueryThresholdMs: number;
116
164
  /** Whether/where to persist debug data to disk across restarts. */
117
165
  persistDebugData: boolean | string;
166
+ /** Whether per-request tracing is enabled. */
167
+ tracing: boolean;
168
+ /** Maximum traces to keep in the ring buffer. */
169
+ maxTraces: number;
118
170
  }
119
171
  /**
120
172
  * Color names available for the `badge` column format.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/debug/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/debug/types.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  position: fixed;
7
7
  inset-inline: 0;
8
8
  bottom: 28px;
9
- z-index: 49;
9
+ z-index: 147;
10
10
  height: 50vh;
11
11
  display: none;
12
12
  flex-direction: column;
@@ -317,7 +317,7 @@
317
317
  display: flex;
318
318
  flex-direction: column;
319
319
  background: #0f0f0f;
320
- z-index: 10;
320
+ z-index: 30;
321
321
  }
322
322
  .ss-dbg-email-preview-header {
323
323
  display: flex;
@@ -363,6 +363,124 @@
363
363
  /* Filterable cell */
364
364
  .ss-dbg-filterable:hover { background: rgba(52, 211, 153, 0.08); }
365
365
 
366
+ /* Timeline */
367
+ .ss-dbg-tl-detail-header {
368
+ display: flex;
369
+ align-items: center;
370
+ gap: 12px;
371
+ padding: 8px 12px;
372
+ border-bottom: 1px solid #262626;
373
+ background: #141414;
374
+ flex-shrink: 0;
375
+ }
376
+ #ss-dbg-tl-waterfall {
377
+ padding: 8px 12px;
378
+ overflow: auto;
379
+ scrollbar-width: none;
380
+ -ms-overflow-style: none;
381
+ }
382
+ #ss-dbg-tl-waterfall::-webkit-scrollbar { display: none; }
383
+
384
+ .ss-dbg-tl-row {
385
+ display: flex;
386
+ align-items: center;
387
+ height: 24px;
388
+ font-size: 11px;
389
+ border-bottom: 1px solid #1a1a1a;
390
+ }
391
+ .ss-dbg-tl-row:hover { background: rgba(38, 38, 38, 0.4); }
392
+ .ss-dbg-tl-label {
393
+ width: 280px;
394
+ min-width: 280px;
395
+ padding-right: 8px;
396
+ overflow: hidden;
397
+ text-overflow: ellipsis;
398
+ white-space: nowrap;
399
+ color: #a3a3a3;
400
+ font-size: 10px;
401
+ }
402
+ .ss-dbg-tl-track {
403
+ flex: 1;
404
+ position: relative;
405
+ height: 16px;
406
+ }
407
+ .ss-dbg-tl-bar {
408
+ position: absolute;
409
+ height: 12px;
410
+ top: 2px;
411
+ border-radius: 2px;
412
+ min-width: 2px;
413
+ cursor: default;
414
+ }
415
+ .ss-dbg-tl-bar-request { background: #1e3a5f; }
416
+ .ss-dbg-tl-bar-middleware { background: #1e3a5f; opacity: 0.7; }
417
+ .ss-dbg-tl-bar-db { background: #6d28d9; }
418
+ .ss-dbg-tl-bar-view { background: #0e7490; }
419
+ .ss-dbg-tl-bar-mail { background: #059669; }
420
+ .ss-dbg-tl-bar-event { background: #b45309; }
421
+ .ss-dbg-tl-bar-custom { background: #525252; }
422
+
423
+ .ss-dbg-tl-dur {
424
+ font-size: 10px;
425
+ color: #525252;
426
+ margin-left: 4px;
427
+ white-space: nowrap;
428
+ font-variant-numeric: tabular-nums;
429
+ }
430
+
431
+ .ss-dbg-tl-legend {
432
+ display: flex;
433
+ gap: 12px;
434
+ padding: 6px 12px;
435
+ border-bottom: 1px solid #262626;
436
+ background: #141414;
437
+ font-size: 10px;
438
+ color: #737373;
439
+ }
440
+ .ss-dbg-tl-legend-item {
441
+ display: flex;
442
+ align-items: center;
443
+ gap: 4px;
444
+ }
445
+ .ss-dbg-tl-legend-dot {
446
+ width: 8px;
447
+ height: 8px;
448
+ border-radius: 2px;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .ss-dbg-tl-warnings {
453
+ margin-top: 12px;
454
+ padding: 8px 12px;
455
+ border-top: 1px solid #262626;
456
+ }
457
+ .ss-dbg-tl-warnings-title {
458
+ font-size: 10px;
459
+ font-weight: 600;
460
+ color: #fbbf24;
461
+ text-transform: uppercase;
462
+ letter-spacing: 0.05em;
463
+ margin-bottom: 4px;
464
+ }
465
+ .ss-dbg-tl-warning {
466
+ font-size: 11px;
467
+ color: #fbbf24;
468
+ padding: 2px 0;
469
+ }
470
+
471
+ .ss-dbg-tl-meta {
472
+ font-size: 10px;
473
+ color: #525252;
474
+ margin-left: 8px;
475
+ }
476
+
477
+ /* Status code badges */
478
+ .ss-dbg-status { font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 3px; }
479
+ .ss-dbg-status-2xx { background: #064e3b; color: #34d399; }
480
+ .ss-dbg-status-3xx { background: #1e3a5f; color: #60a5fa; }
481
+ .ss-dbg-status-4xx { background: #422006; color: #fbbf24; }
482
+ .ss-dbg-status-5xx { background: #450a0a; color: #f87171; }
483
+
366
484
  /* Wrench button in stats bar */
367
485
  .ss-dbg-btn {
368
486
  display: flex;
@@ -18,8 +18,10 @@
18
18
 
19
19
  const LOGS_ENDPOINT = panel.dataset.logsEndpoint || (BASE + '/logs');
20
20
 
21
+ const tracingEnabled = panel.dataset.tracing === '1';
22
+
21
23
  let isOpen = false;
22
- let activeTab = 'queries';
24
+ let activeTab = tracingEnabled ? 'timeline' : 'queries';
23
25
  const fetched = {};
24
26
  let refreshTimer = null;
25
27
  let logFilter = 'all';
@@ -185,7 +187,8 @@
185
187
 
186
188
  // ── Data loading ────────────────────────────────────────────────
187
189
  const loadTab = (name) => {
188
- if (name === 'queries') fetchQueries();
190
+ if (name === 'timeline') fetchTraces();
191
+ else if (name === 'queries') fetchQueries();
189
192
  else if (name === 'events') fetchEvents();
190
193
  else if (name === 'routes' && !fetched.routes) fetchRoutes();
191
194
  else if (name === 'logs') fetchLogs();
@@ -697,6 +700,194 @@
697
700
  });
698
701
  }
699
702
 
703
+ // ── Timeline Tab ────────────────────────────────────────────────
704
+ const tlSearchInput = document.getElementById('ss-dbg-search-timeline');
705
+ const tlSummaryEl = document.getElementById('ss-dbg-timeline-summary');
706
+ const tlBodyEl = document.getElementById('ss-dbg-timeline-body');
707
+ const tlListEl = document.getElementById('ss-dbg-timeline-list');
708
+ const tlDetailEl = document.getElementById('ss-dbg-timeline-detail');
709
+ const tlBackBtn = document.getElementById('ss-dbg-tl-back');
710
+ const tlDetailTitle = document.getElementById('ss-dbg-tl-detail-title');
711
+ const tlWaterfall = document.getElementById('ss-dbg-tl-waterfall');
712
+ let cachedTraces = { traces: [], total: 0 };
713
+
714
+ const statusClass = (code) => {
715
+ if (code >= 500) return 'ss-dbg-status-5xx';
716
+ if (code >= 400) return 'ss-dbg-status-4xx';
717
+ if (code >= 300) return 'ss-dbg-status-3xx';
718
+ return 'ss-dbg-status-2xx';
719
+ };
720
+
721
+ const fetchTraces = () => {
722
+ if (!tracingEnabled) return;
723
+ fetchJSON(BASE + '/traces')
724
+ .then((data) => {
725
+ cachedTraces = data;
726
+ renderTraces();
727
+ })
728
+ .catch(() => {
729
+ if (tlBodyEl) tlBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load traces</div>';
730
+ });
731
+ };
732
+
733
+ const renderTraces = () => {
734
+ if (!tlBodyEl) return;
735
+ const filter = (tlSearchInput ? tlSearchInput.value : '').toLowerCase();
736
+ const traces = cachedTraces.traces || [];
737
+
738
+ if (tlSummaryEl) {
739
+ tlSummaryEl.textContent = cachedTraces.total + ' requests';
740
+ }
741
+
742
+ let filtered = traces;
743
+ if (filter) {
744
+ filtered = traces.filter((t) =>
745
+ t.url.toLowerCase().indexOf(filter) !== -1
746
+ || t.method.toLowerCase().indexOf(filter) !== -1
747
+ );
748
+ }
749
+
750
+ if (filtered.length === 0) {
751
+ tlBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching requests' : 'No requests traced yet') + '</div>';
752
+ return;
753
+ }
754
+
755
+ let html = '<table class="ss-dbg-table"><thead><tr>'
756
+ + '<th style="width:40px">#</th>'
757
+ + '<th style="width:60px">Method</th>'
758
+ + '<th>URL</th>'
759
+ + '<th style="width:55px">Status</th>'
760
+ + '<th style="width:70px">Duration</th>'
761
+ + '<th style="width:50px">Spans</th>'
762
+ + '<th style="width:30px" title="Warnings">&#x26A0;</th>'
763
+ + '<th style="width:70px">Time</th>'
764
+ + '</tr></thead><tbody>';
765
+
766
+ for (let i = 0; i < filtered.length; i++) {
767
+ const t = filtered[i];
768
+ html += '<tr class="ss-dbg-email-row" data-trace-id="' + t.id + '">'
769
+ + '<td style="color:#525252">' + t.id + '</td>'
770
+ + '<td><span class="' + methodClass(t.method) + '">' + esc(t.method) + '</span></td>'
771
+ + '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px" title="' + esc(t.url) + '">' + esc(t.url) + '</td>'
772
+ + '<td><span class="ss-dbg-status ' + statusClass(t.statusCode) + '">' + t.statusCode + '</span></td>'
773
+ + '<td class="ss-dbg-duration ' + durationClass(t.totalDuration) + '">' + t.totalDuration.toFixed(1) + 'ms</td>'
774
+ + '<td style="color:#737373;text-align:center">' + t.spanCount + '</td>'
775
+ + '<td style="text-align:center">' + (t.warningCount > 0 ? '<span style="color:#fbbf24">' + t.warningCount + '</span>' : '<span style="color:#333">-</span>') + '</td>'
776
+ + '<td class="ss-dbg-event-time">' + timeAgo(t.timestamp) + '</td>'
777
+ + '</tr>';
778
+ }
779
+
780
+ html += '</tbody></table>';
781
+ tlBodyEl.innerHTML = html;
782
+
783
+ // Click row to open detail
784
+ tlBodyEl.querySelectorAll('[data-trace-id]').forEach((row) => {
785
+ row.addEventListener('click', () => {
786
+ const id = row.getAttribute('data-trace-id');
787
+ fetchTraceDetail(id);
788
+ });
789
+ });
790
+ };
791
+
792
+ const fetchTraceDetail = (id) => {
793
+ fetchJSON(BASE + '/traces/' + id)
794
+ .then((trace) => {
795
+ showTimeline(trace);
796
+ })
797
+ .catch(() => {
798
+ if (tlWaterfall) tlWaterfall.innerHTML = '<div class="ss-dbg-empty">Failed to load trace</div>';
799
+ });
800
+ };
801
+
802
+ const showTimeline = (trace) => {
803
+ if (!tlListEl || !tlDetailEl || !tlDetailTitle || !tlWaterfall) return;
804
+
805
+ tlListEl.style.display = 'none';
806
+ tlDetailEl.style.display = '';
807
+
808
+ tlDetailTitle.innerHTML =
809
+ '<span class="' + methodClass(trace.method) + '">' + esc(trace.method) + '</span> '
810
+ + esc(trace.url) + ' '
811
+ + '<span class="ss-dbg-status ' + statusClass(trace.statusCode) + '">' + trace.statusCode + '</span>'
812
+ + '<span class="ss-dbg-tl-meta">' + trace.totalDuration.toFixed(1) + 'ms &middot; '
813
+ + trace.spanCount + ' spans &middot; '
814
+ + formatTime(trace.timestamp) + '</span>';
815
+
816
+ const spans = trace.spans || [];
817
+ const total = trace.totalDuration || 1;
818
+
819
+ // Legend
820
+ let html = '<div class="ss-dbg-tl-legend">'
821
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#6d28d9"></span>DB</div>'
822
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#1e3a5f"></span>Request</div>'
823
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#059669"></span>Mail</div>'
824
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#b45309"></span>Event</div>'
825
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#0e7490"></span>View</div>'
826
+ + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#525252"></span>Custom</div>'
827
+ + '</div>';
828
+
829
+ if (spans.length === 0) {
830
+ html += '<div class="ss-dbg-empty">No spans captured for this request</div>';
831
+ } else {
832
+ // Build nesting depth from parentId
833
+ const depthMap = {};
834
+ for (let i = 0; i < spans.length; i++) {
835
+ const s = spans[i];
836
+ if (!s.parentId) {
837
+ depthMap[s.id] = 0;
838
+ } else {
839
+ depthMap[s.id] = (depthMap[s.parentId] || 0) + 1;
840
+ }
841
+ }
842
+
843
+ // Sort by startOffset
844
+ const sorted = spans.slice().sort((a, b) => a.startOffset - b.startOffset);
845
+
846
+ for (let i = 0; i < sorted.length; i++) {
847
+ const s = sorted[i];
848
+ const depth = depthMap[s.id] || 0;
849
+ const leftPct = (s.startOffset / total * 100).toFixed(2);
850
+ const widthPct = Math.max(s.duration / total * 100, 0.5).toFixed(2);
851
+ const indent = depth * 16;
852
+ const catLabel = s.category === 'db' ? 'DB' : s.category;
853
+ const metaStr = s.metadata ? Object.entries(s.metadata).filter(([,v]) => v != null).map(([k,v]) => k + '=' + v).join(', ') : '';
854
+ const tooltip = s.label + ' (' + s.duration.toFixed(2) + 'ms)' + (metaStr ? '\n' + metaStr : '');
855
+
856
+ html += '<div class="ss-dbg-tl-row">'
857
+ + '<div class="ss-dbg-tl-label" style="padding-left:' + (8 + indent) + 'px" title="' + esc(tooltip) + '">'
858
+ + '<span class="ss-dbg-badge ss-dbg-badge-' + (s.category === 'db' ? 'purple' : s.category === 'mail' ? 'green' : s.category === 'event' ? 'amber' : s.category === 'view' ? 'blue' : 'muted') + '" style="font-size:9px;margin-right:4px">' + esc(catLabel) + '</span>'
859
+ + esc(s.label.length > 40 ? s.label.slice(0, 40) + '...' : s.label)
860
+ + '</div>'
861
+ + '<div class="ss-dbg-tl-track">'
862
+ + '<div class="ss-dbg-tl-bar ss-dbg-tl-bar-' + esc(s.category) + '" style="left:' + leftPct + '%;width:' + widthPct + '%" title="' + esc(tooltip) + '"></div>'
863
+ + '</div>'
864
+ + '<span class="ss-dbg-tl-dur">' + s.duration.toFixed(2) + 'ms</span>'
865
+ + '</div>';
866
+ }
867
+ }
868
+
869
+ // Warnings
870
+ if (trace.warnings && trace.warnings.length > 0) {
871
+ html += '<div class="ss-dbg-tl-warnings">'
872
+ + '<div class="ss-dbg-tl-warnings-title">Warnings (' + trace.warnings.length + ')</div>';
873
+ for (let w = 0; w < trace.warnings.length; w++) {
874
+ html += '<div class="ss-dbg-tl-warning">' + esc(trace.warnings[w]) + '</div>';
875
+ }
876
+ html += '</div>';
877
+ }
878
+
879
+ tlWaterfall.innerHTML = html;
880
+ };
881
+
882
+ if (tlBackBtn) {
883
+ tlBackBtn.addEventListener('click', () => {
884
+ if (tlListEl) tlListEl.style.display = '';
885
+ if (tlDetailEl) tlDetailEl.style.display = 'none';
886
+ });
887
+ }
888
+
889
+ if (tlSearchInput) tlSearchInput.addEventListener('input', renderTraces);
890
+
700
891
  // ── Custom panes: fetch, render, bind ───────────────────────────
701
892
  const getNestedValue = (obj, path) => {
702
893
  const parts = path.split('.');
@@ -6,7 +6,7 @@
6
6
  position: fixed;
7
7
  inset-inline: 0;
8
8
  bottom: 0;
9
- z-index: 50;
9
+ z-index: 150;
10
10
  display: flex;
11
11
  height: 28px;
12
12
  align-items: center;
@@ -102,7 +102,7 @@
102
102
  .ss-toggle {
103
103
  position: fixed;
104
104
  right: 12px;
105
- z-index: 50;
105
+ z-index: 150;
106
106
  display: flex;
107
107
  align-items: center;
108
108
  gap: 6px;
@@ -139,7 +139,7 @@
139
139
  /* Tooltip — positioned absolutely within .ss-bar (outside scroll area) via JS */
140
140
  .ss-tooltip {
141
141
  position: absolute;
142
- z-index: 60;
142
+ z-index: 180;
143
143
  pointer-events: none;
144
144
  }
145
145
  .ss-tooltip.ss-pinned {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/edge/plugin.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,IACrD,MAAM,GAAG,UA+HlB"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/edge/plugin.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,IACrD,MAAM,GAAG,UAgIlB"}
@@ -98,6 +98,7 @@ export function edgePluginServerStats(config) {
98
98
  state.debugJs = read("client/debug-panel.js");
99
99
  state.logsEndpoint = "/admin/api/debug/logs";
100
100
  state.customPanes = config.devToolbar?.panes || [];
101
+ state.showTracing = !!config.devToolbar?.tracing;
101
102
  }
102
103
  // Pre-render via Template directly — bypasses edge.createRenderer() which
103
104
  // would re-run #executePlugins and cause infinite recursion.
@@ -1,6 +1,11 @@
1
- <div id="ss-dbg-panel" class="ss-dbg-panel" data-logs-endpoint="{{ logsEndpoint }}">
1
+ <div id="ss-dbg-panel" class="ss-dbg-panel" data-logs-endpoint="{{ logsEndpoint }}" data-tracing="{{ showTracing ? '1' : '0' }}">
2
2
  <div class="ss-dbg-tabs">
3
- <button type="button" class="ss-dbg-tab ss-dbg-active" data-ss-dbg-tab="queries">Queries</button>
3
+ @if(showTracing)
4
+ <button type="button" class="ss-dbg-tab ss-dbg-active" data-ss-dbg-tab="timeline">Timeline</button>
5
+ <button type="button" class="ss-dbg-tab" data-ss-dbg-tab="queries">Queries</button>
6
+ @else
7
+ <button type="button" class="ss-dbg-tab ss-dbg-active" data-ss-dbg-tab="queries">Queries</button>
8
+ @end
4
9
  <button type="button" class="ss-dbg-tab" data-ss-dbg-tab="events">Events</button>
5
10
  <button type="button" class="ss-dbg-tab" data-ss-dbg-tab="routes">Routes</button>
6
11
  <button type="button" class="ss-dbg-tab" data-ss-dbg-tab="logs">Logs</button>
@@ -14,7 +19,25 @@
14
19
  </div>
15
20
  </div>
16
21
  <div class="ss-dbg-content">
17
- <div id="ss-dbg-pane-queries" class="ss-dbg-pane ss-dbg-active">
22
+ @if(showTracing)
23
+ <div id="ss-dbg-pane-timeline" class="ss-dbg-pane ss-dbg-active">
24
+ <div id="ss-dbg-timeline-list">
25
+ <div class="ss-dbg-search-bar">
26
+ <input type="text" class="ss-dbg-search" id="ss-dbg-search-timeline" placeholder="Filter by URL or method..." />
27
+ <span id="ss-dbg-timeline-summary" class="ss-dbg-summary"></span>
28
+ </div>
29
+ <div id="ss-dbg-timeline-body"><div class="ss-dbg-empty">Loading traces...</div></div>
30
+ </div>
31
+ <div id="ss-dbg-timeline-detail" style="display:none">
32
+ <div class="ss-dbg-tl-detail-header">
33
+ <button type="button" class="ss-dbg-btn-clear" id="ss-dbg-tl-back">&larr; Back</button>
34
+ <span id="ss-dbg-tl-detail-title" class="ss-dbg-summary"></span>
35
+ </div>
36
+ <div id="ss-dbg-tl-waterfall"></div>
37
+ </div>
38
+ </div>
39
+ @end
40
+ <div id="ss-dbg-pane-queries" class="ss-dbg-pane{{ showTracing ? '' : ' ss-dbg-active' }}">
18
41
  <div class="ss-dbg-search-bar">
19
42
  <input type="text" class="ss-dbg-search" id="ss-dbg-search-queries" placeholder="Filter queries by SQL, model, or method..." />
20
43
  <span id="ss-dbg-queries-summary" class="ss-dbg-summary"></span>
@@ -1,7 +1,8 @@
1
1
  export { defineConfig } from './define_config.js';
2
2
  export { StatsEngine } from './engine/stats_engine.js';
3
3
  export { RequestMetrics } from './engine/request_metrics.js';
4
+ export { trace } from './debug/trace_collector.js';
4
5
  export type { MetricCollector } from './collectors/collector.js';
5
6
  export type { MetricValue, ServerStats, ServerStatsConfig, LogStats, DevToolbarOptions } from './types.js';
6
- export type { DebugPane, DebugPaneColumn, DebugPaneFormatType, DebugPaneSearch, BadgeColor, QueryRecord, EventRecord, EmailRecord, RouteRecord, DevToolbarConfig, } from './debug/types.js';
7
+ export type { DebugPane, DebugPaneColumn, DebugPaneFormatType, DebugPaneSearch, BadgeColor, QueryRecord, EventRecord, EmailRecord, RouteRecord, TraceSpan, TraceRecord, DevToolbarConfig, } from './debug/types.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC1G,YAAY,EACV,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AAClD,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC1G,YAAY,EACV,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,gBAAgB,GACjB,MAAM,kBAAkB,CAAA"}
package/dist/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { defineConfig } from './define_config.js';
2
2
  export { StatsEngine } from './engine/stats_engine.js';
3
3
  export { RequestMetrics } from './engine/request_metrics.js';
4
+ export { trace } from './debug/trace_collector.js';
@@ -1,6 +1,8 @@
1
1
  import type { HttpContext } from "@adonisjs/core/http";
2
2
  import type { NextFn } from "@adonisjs/core/types/http";
3
+ import type { TraceCollector } from "../debug/trace_collector.js";
3
4
  export declare function setShouldShow(fn: ((ctx: any) => boolean) | null): void;
5
+ export declare function setTraceCollector(collector: TraceCollector | null): void;
4
6
  export default class RequestTrackingMiddleware {
5
7
  handle(ctx: HttpContext, next: NextFn): Promise<void>;
6
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"request_tracking_middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/request_tracking_middleware.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAOxD,wBAAgB,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,IAAI,QAE/D;AAED,MAAM,CAAC,OAAO,OAAO,yBAAyB;IACtC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM;CA8B5C"}
1
+ {"version":3,"file":"request_tracking_middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/request_tracking_middleware.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAOlE,wBAAgB,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,IAAI,QAE/D;AAOD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,QAEjE;AAED,MAAM,CAAC,OAAO,OAAO,yBAAyB;IACtC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM;CA4C5C"}
@@ -7,6 +7,13 @@ let shouldShowFn = null;
7
7
  export function setShouldShow(fn) {
8
8
  shouldShowFn = fn;
9
9
  }
10
+ /**
11
+ * Module-level trace collector, set by the provider when tracing is enabled.
12
+ */
13
+ let traceCollector = null;
14
+ export function setTraceCollector(collector) {
15
+ traceCollector = collector;
16
+ }
10
17
  export default class RequestTrackingMiddleware {
11
18
  async handle(ctx, next) {
12
19
  const metrics = getRequestMetrics();
@@ -29,13 +36,22 @@ export default class RequestTrackingMiddleware {
29
36
  },
30
37
  });
31
38
  }
32
- try {
33
- await next();
39
+ const runRequest = async () => {
40
+ try {
41
+ await next();
42
+ }
43
+ finally {
44
+ const duration = performance.now() - start;
45
+ metrics.decrementActiveConnections();
46
+ metrics.recordRequest(duration, ctx.response.getStatus());
47
+ traceCollector?.finishTrace(ctx.request.method(), ctx.request.url(true), ctx.response.getStatus());
48
+ }
49
+ };
50
+ if (traceCollector) {
51
+ await traceCollector.startTrace(runRequest);
34
52
  }
35
- finally {
36
- const duration = performance.now() - start;
37
- metrics.decrementActiveConnections();
38
- metrics.recordRequest(duration, ctx.response.getStatus());
53
+ else {
54
+ await runRequest();
39
55
  }
40
56
  }
41
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAO1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAN7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAA+C;gBAE3C,GAAG,EAAE,kBAAkB;IAEvC,IAAI;IAoBJ,KAAK;YAoEG,eAAe;IA+CvB,QAAQ;CAuBf"}
1
+ {"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAO1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAN7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAA+C;gBAE3C,GAAG,EAAE,kBAAkB;IAEvC,IAAI;IAoBJ,KAAK;YAsEG,eAAe;IAoDvB,QAAQ;CAuBf"}
@@ -1,6 +1,6 @@
1
1
  import { StatsEngine } from "../engine/stats_engine.js";
2
2
  import { DebugStore } from "../debug/debug_store.js";
3
- import { setShouldShow } from "../middleware/request_tracking_middleware.js";
3
+ import { setShouldShow, setTraceCollector } from "../middleware/request_tracking_middleware.js";
4
4
  export default class ServerStatsProvider {
5
5
  app;
6
6
  intervalId = null;
@@ -50,6 +50,8 @@ export default class ServerStatsProvider {
50
50
  maxEmails: toolbarConfig.maxEmails ?? 100,
51
51
  slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
52
52
  persistDebugData: toolbarConfig.persistDebugData ?? false,
53
+ tracing: toolbarConfig.tracing ?? false,
54
+ maxTraces: toolbarConfig.maxTraces ?? 200,
53
55
  });
54
56
  }
55
57
  let transmit = null;
@@ -113,6 +115,10 @@ export default class ServerStatsProvider {
113
115
  // Router not available
114
116
  }
115
117
  await this.debugStore.start(emitter, router);
118
+ // Wire trace collector into the request tracking middleware
119
+ if (this.debugStore.traces) {
120
+ setTraceCollector(this.debugStore.traces);
121
+ }
116
122
  // Periodic flush every 30 seconds (handles crashes)
117
123
  if (this.persistPath) {
118
124
  this.flushTimer = setInterval(async () => {
@@ -230,6 +230,21 @@ export interface DevToolbarOptions {
230
230
  * @default false
231
231
  */
232
232
  persistDebugData?: boolean | string;
233
+ /**
234
+ * Enable per-request tracing with timeline visualization.
235
+ *
236
+ * When enabled, each HTTP request is traced with a waterfall
237
+ * of all operations (DB queries, events, mail) that occurred
238
+ * during the request.
239
+ *
240
+ * @default false
241
+ */
242
+ tracing?: boolean;
243
+ /**
244
+ * Maximum number of request traces to keep in the ring buffer.
245
+ * @default 200
246
+ */
247
+ maxTraces?: number;
233
248
  }
234
249
  /**
235
250
  * Top-level configuration for `adonisjs-server-stats`.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAMnD;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAG1B,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAA;IAEnB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAA;IAEd,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAA;IAEnB,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAA;IAEpB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAA;IAIjB;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,CAAA;IAEzB;;;;;;;;OAQG;IACH,iBAAiB,EAAE,MAAM,CAAA;IAEzB;;;;;;;;;;OAUG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB,iDAAiD;IACjD,qBAAqB,EAAE,MAAM,CAAA;IAI7B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAElB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAA;IAElB,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAA;IAErB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAA;IAIjB,sDAAsD;IACtD,OAAO,EAAE,OAAO,CAAA;IAEhB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAA;IAEzB,sDAAsD;IACtD,qBAAqB,EAAE,MAAM,CAAA;IAE7B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAA;IAEtB;;;;;;;;;;OAUG;IACH,YAAY,EAAE,MAAM,CAAA;IAIpB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAA;IAEnB,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAA;IAEpB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAA;IAEpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;IAEnB,gEAAgE;IAChE,gBAAgB,EAAE,MAAM,CAAA;IAIxB,8CAA8C;IAC9C,eAAe,EAAE,MAAM,CAAA;IAEvB,+CAA+C;IAC/C,eAAe,EAAE,MAAM,CAAA;IAEvB,gDAAgD;IAChD,gBAAgB,EAAE,MAAM,CAAA;IAExB,iDAAiD;IACjD,mBAAmB,EAAE,MAAM,CAAA;IAE3B,gDAAgD;IAChD,kBAAkB,EAAE,MAAM,CAAA;IAE1B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAA;IAIpB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IAEnB,wDAAwD;IACxD,eAAe,EAAE,MAAM,CAAA;IAEvB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAA;IAIrB,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAA;IAEvB,0DAA0D;IAC1D,iBAAiB,EAAE,MAAM,CAAA;IAEzB,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,CAAA;IAExB,gEAAgE;IAChE,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAMD;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAA;IAEpB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAA;IAEtB,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAA;IAErB,oDAAoD;IACpD,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAA;IAEhB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAE7B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;IAEnB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CACpC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;;;;;;;;OASG;IACH,SAAS,EAAE,UAAU,GAAG,MAAM,CAAA;IAE9B;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,CAAA;IAEnB;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAA;IAExB;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,EAAE,eAAe,EAAE,CAAA;IAE7B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;IAE/C;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAE9B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAA;CACnC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adonisjs-server-stats",
3
- "version": "1.1.4",
3
+ "version": "1.2.2",
4
4
  "description": "Real-time server monitoring for AdonisJS v6 applications",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",