nuxt-devtools-observatory 0.1.30 → 0.1.31

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 (39) hide show
  1. package/README.md +63 -4
  2. package/client/.env +2 -1
  3. package/client/.env.example +2 -1
  4. package/client/dist/assets/index-BuMXDBO9.js +17 -0
  5. package/client/dist/assets/{index-BmGW_M3W.css → index-CwcspZ6w.css} +1 -1
  6. package/client/dist/index.html +2 -2
  7. package/client/src/App.vue +4 -0
  8. package/client/src/components/Flamegraph.vue +443 -0
  9. package/client/src/components/SpanInspector.vue +446 -0
  10. package/client/src/components/TraceFilter.vue +344 -0
  11. package/client/src/components/WaterfallView.vue +443 -0
  12. package/client/src/composables/useTraceFilter.ts +164 -0
  13. package/client/src/stores/observatory.ts +5 -1
  14. package/client/src/views/TraceViewer.vue +599 -0
  15. package/client/src/views/TransitionTimeline.vue +1 -6
  16. package/dist/module.d.mts +5 -0
  17. package/dist/module.json +1 -1
  18. package/dist/module.mjs +30 -44
  19. package/dist/runtime/composables/render-registry.js +66 -110
  20. package/dist/runtime/composables/transition-registry.js +103 -28
  21. package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
  22. package/dist/runtime/instrumentation/asyncData.js +49 -0
  23. package/dist/runtime/instrumentation/component.d.ts +2 -0
  24. package/dist/runtime/instrumentation/component.js +126 -0
  25. package/dist/runtime/instrumentation/fetch.d.ts +2 -0
  26. package/dist/runtime/instrumentation/fetch.js +89 -0
  27. package/dist/runtime/instrumentation/route.d.ts +6 -0
  28. package/dist/runtime/instrumentation/route.js +41 -0
  29. package/dist/runtime/plugin.js +38 -2
  30. package/dist/runtime/tracing/context.d.ts +9 -0
  31. package/dist/runtime/tracing/context.js +15 -0
  32. package/dist/runtime/tracing/trace.d.ts +25 -0
  33. package/dist/runtime/tracing/trace.js +0 -0
  34. package/dist/runtime/tracing/traceStore.d.ts +39 -0
  35. package/dist/runtime/tracing/traceStore.js +101 -0
  36. package/dist/runtime/tracing/tracing.d.ts +27 -0
  37. package/dist/runtime/tracing/tracing.js +48 -0
  38. package/package.json +1 -1
  39. package/client/dist/assets/index-BCaKoHBH.js +0 -17
@@ -0,0 +1,599 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import { useObservatoryData } from '@observatory-client/stores/observatory'
4
+ import Flamegraph from '@observatory-client/components/Flamegraph.vue'
5
+ import WaterfallView from '@observatory-client/components/WaterfallView.vue'
6
+ import SpanInspector from '@observatory-client/components/SpanInspector.vue'
7
+ import TraceFilter from '@observatory-client/components/TraceFilter.vue'
8
+ import { useTraceFilter } from '@observatory-client/composables/useTraceFilter'
9
+ import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
10
+
11
+ const { traces, connected } = useObservatoryData()
12
+
13
+ const selectedTraceId = ref<string | null>(null)
14
+ const selectedSpan = ref<TraceSpan | undefined>(undefined)
15
+ const viewMode = ref<'overview' | 'flamegraph' | 'waterfall'>('overview')
16
+ const showFilters = ref(false)
17
+
18
+ const {
19
+ searchQuery,
20
+ selectedSpanTypes,
21
+ minDuration,
22
+ maxDuration,
23
+ routeFilter,
24
+ filterTraces: applyFilters,
25
+ clearFilters,
26
+ hasActiveFilters,
27
+ } = useTraceFilter()
28
+
29
+ const sortedTraces = computed(() => {
30
+ return [...traces.value].sort((a, b) => b.startTime - a.startTime)
31
+ })
32
+
33
+ const filteredTraces = computed(() => {
34
+ return applyFilters(sortedTraces.value)
35
+ })
36
+
37
+ const traceCountLabel = computed(() => {
38
+ if (!hasActiveFilters.value) {
39
+ return `${sortedTraces.value.length} traces`
40
+ }
41
+
42
+ return `Showing ${filteredTraces.value.length} of ${sortedTraces.value.length} traces`
43
+ })
44
+
45
+ const selectedTrace = computed(() => {
46
+ if (!selectedTraceId.value) {
47
+ return filteredTraces.value[0]
48
+ }
49
+
50
+ return filteredTraces.value.find((t) => t.id === selectedTraceId.value)
51
+ })
52
+
53
+ function elapsedFromSpans(trace: TraceEntry): number | undefined {
54
+ if (!trace.spans.length) {
55
+ return undefined
56
+ }
57
+
58
+ let max = 0
59
+
60
+ for (const span of trace.spans) {
61
+ const end = span.endTime ?? span.startTime + (span.durationMs ?? 0)
62
+ const offset = end - trace.startTime
63
+
64
+ if (offset > max) {
65
+ max = offset
66
+ }
67
+ }
68
+
69
+ return max > 0 ? max : undefined
70
+ }
71
+
72
+ function formatDuration(durationMs?: number, trace?: TraceEntry): string {
73
+ if (durationMs !== undefined) {
74
+ return `${Math.round(durationMs * 10) / 10}ms`
75
+ }
76
+
77
+ if (trace) {
78
+ const elapsed = elapsedFromSpans(trace)
79
+
80
+ if (elapsed !== undefined) {
81
+ return `~${Math.round(elapsed * 10) / 10}ms`
82
+ }
83
+ }
84
+
85
+ return 'active'
86
+ }
87
+
88
+ function asString(val: unknown): string {
89
+ return typeof val === 'string' ? val : ''
90
+ }
91
+
92
+ function getSpanDisplayName(span: TraceSpan): string {
93
+ const m = span.metadata as Record<string, unknown> | undefined
94
+
95
+ if (!m) {
96
+ return span.name
97
+ }
98
+
99
+ const componentName = asString(m.componentName)
100
+ const lifecycle = asString(m.lifecycle)
101
+ const uid = m.uid !== undefined ? String(m.uid) : ''
102
+ const method = asString(m.method)
103
+ const url = asString(m.url)
104
+ const route = asString(m.route) || asString(m.path)
105
+
106
+ if (componentName && lifecycle) {
107
+ return uid ? `${componentName}.${lifecycle} #${uid}` : `${componentName}.${lifecycle}`
108
+ }
109
+
110
+ if (componentName) {
111
+ return uid ? `${componentName} #${uid}` : componentName
112
+ }
113
+
114
+ if (method && url) {
115
+ return `${method} ${url}`
116
+ }
117
+
118
+ if (url) {
119
+ return url
120
+ }
121
+
122
+ if (route) {
123
+ return `${span.name} (${route})`
124
+ }
125
+
126
+ return span.name
127
+ }
128
+
129
+ function selectTrace(trace: TraceEntry) {
130
+ selectedTraceId.value = trace.id
131
+ selectedSpan.value = undefined
132
+ }
133
+
134
+ function handleClearFilters() {
135
+ clearFilters()
136
+ selectedSpan.value = undefined
137
+ }
138
+
139
+ function handleSpanTypesUpdate(value: Set<string>) {
140
+ selectedSpanTypes.value = value
141
+ }
142
+ </script>
143
+
144
+ <template>
145
+ <div class="trace-viewer tracker-view">
146
+ <!-- Header -->
147
+ <div class="trace-viewer__header tracker-toolbar">
148
+ <div class="trace-viewer__title">Trace Viewer</div>
149
+ <div class="trace-viewer__header-actions">
150
+ <div class="trace-viewer__count muted text-sm">{{ traceCountLabel }}</div>
151
+ <button
152
+ :class="{ 'trace-viewer__filter-btn--active': showFilters }"
153
+ class="trace-viewer__filter-btn"
154
+ title="Toggle filters"
155
+ @click="showFilters = !showFilters"
156
+ >
157
+ 🔍
158
+ </button>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Filters panel -->
163
+ <TraceFilter
164
+ v-if="showFilters"
165
+ :traces="sortedTraces"
166
+ :search-query="searchQuery"
167
+ :selected-span-types="selectedSpanTypes"
168
+ :min-duration="minDuration"
169
+ :max-duration="maxDuration"
170
+ :route-filter="routeFilter"
171
+ :has-active-filters="hasActiveFilters"
172
+ @update:search="searchQuery = $event"
173
+ @update:types="handleSpanTypesUpdate"
174
+ @update:min-duration="minDuration = $event"
175
+ @update:max-duration="maxDuration = $event"
176
+ @update:route="routeFilter = $event"
177
+ @clear-filters="handleClearFilters"
178
+ />
179
+
180
+ <!-- Main content -->
181
+ <div class="trace-viewer__container">
182
+ <!-- Trace list / Preview -->
183
+ <div class="trace-viewer__list">
184
+ <div class="trace-viewer__list-header">
185
+ <span class="trace-viewer__list-title">Traces</span>
186
+ </div>
187
+ <div class="trace-viewer__table-wrap tracker-table-wrap">
188
+ <table class="data-table">
189
+ <thead>
190
+ <tr>
191
+ <th>Trace Name</th>
192
+ <th>Duration</th>
193
+ <th>Spans</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ <tr
198
+ v-for="trace in filteredTraces"
199
+ :key="trace.id"
200
+ :class="{ 'trace-viewer__trace-row--selected': selectedTrace?.id === trace.id }"
201
+ class="trace-viewer__trace-row"
202
+ @click="selectTrace(trace)"
203
+ >
204
+ <td class="mono">{{ trace.name }}</td>
205
+ <td class="mono">{{ formatDuration(trace.durationMs, trace) }}</td>
206
+ <td class="mono">{{ trace.spans.length }}</td>
207
+ </tr>
208
+ <tr v-if="!filteredTraces.length">
209
+ <td colspan="3" class="tracker-empty-cell">
210
+ {{
211
+ sortedTraces.length === 0
212
+ ? connected
213
+ ? 'No traces recorded yet.'
214
+ : 'Waiting for connection to the Nuxt app…'
215
+ : 'No traces match applied filters.'
216
+ }}
217
+ </td>
218
+ </tr>
219
+ </tbody>
220
+ </table>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Visualization area + Inspector -->
225
+ <div v-if="selectedTrace" class="trace-viewer__detail">
226
+ <div class="trace-viewer__detail-content">
227
+ <!-- View mode tabs -->
228
+ <div class="trace-viewer__tabs">
229
+ <button
230
+ :class="{ 'trace-viewer__tab--active': viewMode === 'overview' }"
231
+ class="trace-viewer__tab"
232
+ @click="viewMode = 'overview'"
233
+ >
234
+ Overview
235
+ </button>
236
+ <button
237
+ :class="{ 'trace-viewer__tab--active': viewMode === 'flamegraph' }"
238
+ class="trace-viewer__tab"
239
+ @click="viewMode = 'flamegraph'"
240
+ >
241
+ Flamegraph
242
+ </button>
243
+ <button
244
+ :class="{ 'trace-viewer__tab--active': viewMode === 'waterfall' }"
245
+ class="trace-viewer__tab"
246
+ @click="viewMode = 'waterfall'"
247
+ >
248
+ Waterfall
249
+ </button>
250
+ </div>
251
+
252
+ <!-- Visualization -->
253
+ <div class="trace-viewer__visualization">
254
+ <!-- Overview -->
255
+ <div v-if="viewMode === 'overview'" class="trace-viewer__overview">
256
+ <div class="trace-viewer__overview-header">
257
+ <div>
258
+ <div class="trace-viewer__overview-label">Trace</div>
259
+ <div class="trace-viewer__overview-value">{{ selectedTrace.name }}</div>
260
+ </div>
261
+ <div>
262
+ <div class="trace-viewer__overview-label">Duration</div>
263
+ <div class="trace-viewer__overview-value">
264
+ {{ formatDuration(selectedTrace.durationMs, selectedTrace) }}
265
+ </div>
266
+ </div>
267
+ <div>
268
+ <div class="trace-viewer__overview-label">Spans</div>
269
+ <div class="trace-viewer__overview-value">{{ selectedTrace.spans.length }}</div>
270
+ </div>
271
+ <div>
272
+ <div class="trace-viewer__overview-label">Status</div>
273
+ <div class="trace-viewer__overview-value">{{ selectedTrace.status }}</div>
274
+ </div>
275
+ </div>
276
+
277
+ <div class="trace-viewer__span-list">
278
+ <div
279
+ v-for="span in selectedTrace.spans"
280
+ :key="span.id"
281
+ :class="{ 'trace-viewer__span-item--selected': selectedSpan?.id === span.id }"
282
+ class="trace-viewer__span-item"
283
+ @click="selectedSpan = span"
284
+ >
285
+ <div class="trace-viewer__span-name">{{ getSpanDisplayName(span) }}</div>
286
+ <div class="trace-viewer__span-meta">
287
+ <span class="trace-viewer__span-type">{{ span.type }}</span>
288
+ <span class="trace-viewer__span-duration">{{ formatDuration(span.durationMs) }}</span>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <!-- Flamegraph -->
295
+ <div v-if="viewMode === 'flamegraph'" class="trace-viewer__flamegraph">
296
+ <Flamegraph :trace="selectedTrace" :selected-span-id="selectedSpan?.id" @select-span="selectedSpan = $event" />
297
+ </div>
298
+
299
+ <!-- Waterfall -->
300
+ <div v-if="viewMode === 'waterfall'" class="trace-viewer__waterfall">
301
+ <WaterfallView :trace="selectedTrace" />
302
+ </div>
303
+ </div>
304
+ </div>
305
+
306
+ <!-- Inspector sidebar -->
307
+ <div class="trace-viewer__inspector">
308
+ <SpanInspector :trace="selectedTrace" :span="selectedSpan" @select-span="selectedSpan = $event" />
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </template>
314
+
315
+ <style scoped>
316
+ .trace-viewer {
317
+ display: flex;
318
+ flex-direction: column;
319
+ height: 100%;
320
+ overflow: hidden;
321
+ }
322
+
323
+ .trace-viewer__header {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: space-between;
327
+ flex-shrink: 0;
328
+ }
329
+
330
+ .trace-viewer__title {
331
+ font-size: 13px;
332
+ font-weight: 600;
333
+ color: var(--text);
334
+ }
335
+
336
+ .trace-viewer__header-actions {
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 12px;
340
+ }
341
+
342
+ .trace-viewer__count {
343
+ font-family: var(--mono);
344
+ }
345
+
346
+ .trace-viewer__filter-btn {
347
+ padding: 4px 8px;
348
+ background: none;
349
+ border: 1px solid transparent;
350
+ color: var(--text-secondary);
351
+ cursor: pointer;
352
+ font-size: 14px;
353
+ transition: all 0.2s;
354
+ border-radius: 3px;
355
+ }
356
+
357
+ .trace-viewer__filter-btn:hover {
358
+ background: var(--bg-secondary);
359
+ border-color: var(--border);
360
+ }
361
+
362
+ .trace-viewer__filter-btn--active {
363
+ background: var(--accent-bg);
364
+ border-color: var(--accent);
365
+ color: var(--accent);
366
+ }
367
+
368
+ .trace-viewer__container {
369
+ display: flex;
370
+ flex: 1;
371
+ min-height: 0;
372
+ gap: 1px;
373
+ background: var(--border);
374
+ }
375
+
376
+ .trace-viewer__list {
377
+ display: flex;
378
+ flex-direction: column;
379
+ width: 280px;
380
+ min-width: 280px;
381
+ background: var(--bg);
382
+ border-right: 1px solid var(--border);
383
+ overflow: hidden;
384
+ }
385
+
386
+ .trace-viewer__list-header {
387
+ padding: 8px 12px;
388
+ font-size: 12px;
389
+ font-weight: 600;
390
+ color: var(--text-secondary);
391
+ text-transform: uppercase;
392
+ letter-spacing: 0.5px;
393
+ border-bottom: 1px solid var(--border);
394
+ flex-shrink: 0;
395
+ }
396
+
397
+ .trace-viewer__table-wrap {
398
+ flex: 1;
399
+ overflow: auto;
400
+ }
401
+
402
+ .trace-viewer__trace-row {
403
+ cursor: pointer;
404
+ transition: background 0.2s;
405
+ }
406
+
407
+ .trace-viewer__trace-row:hover {
408
+ background: var(--bg2, var(--bg));
409
+ }
410
+
411
+ .trace-viewer__trace-row--selected {
412
+ background: var(--bg2, var(--bg));
413
+ border-left: 2px solid var(--accent);
414
+ }
415
+
416
+ .trace-viewer__detail {
417
+ flex: 1;
418
+ display: flex;
419
+ min-width: 0;
420
+ background: var(--bg);
421
+ overflow: hidden;
422
+ }
423
+
424
+ .trace-viewer__detail-content {
425
+ flex: 1;
426
+ display: flex;
427
+ flex-direction: column;
428
+ min-width: 0;
429
+ overflow: hidden;
430
+ }
431
+
432
+ .trace-viewer__tabs {
433
+ display: flex;
434
+ gap: 0;
435
+ border-bottom: 0.5px solid var(--border);
436
+ background: var(--bg3);
437
+ flex-shrink: 0;
438
+ padding: 8px 4px 0;
439
+ }
440
+
441
+ .trace-viewer__tab {
442
+ padding: 6px 12px 8px;
443
+ background: transparent;
444
+ border: none;
445
+ border-bottom: 2px solid transparent;
446
+ margin-bottom: -1px;
447
+ color: var(--text3);
448
+ cursor: pointer;
449
+ font-size: 12px;
450
+ font-weight: 500;
451
+ transition:
452
+ color 0.12s,
453
+ border-color 0.12s;
454
+ border-radius: 0;
455
+ }
456
+
457
+ .trace-viewer__tab:hover {
458
+ color: var(--text);
459
+ background: transparent;
460
+ }
461
+
462
+ .trace-viewer__tab--active {
463
+ color: var(--purple);
464
+ border-bottom-color: var(--purple);
465
+ background: transparent;
466
+ }
467
+
468
+ .trace-viewer__visualization {
469
+ flex: 1;
470
+ overflow: hidden;
471
+ min-height: 0;
472
+ }
473
+
474
+ .trace-viewer__overview {
475
+ display: flex;
476
+ flex-direction: column;
477
+ height: 100%;
478
+ overflow: hidden;
479
+ }
480
+
481
+ .trace-viewer__overview-header {
482
+ display: flex;
483
+ gap: 24px;
484
+ padding: 16px;
485
+ border-bottom: 1px solid var(--border);
486
+ background: var(--bg2, var(--bg));
487
+ flex-shrink: 0;
488
+ }
489
+
490
+ .trace-viewer__overview-label {
491
+ font-size: 10px;
492
+ font-weight: 600;
493
+ color: var(--text2, var(--text));
494
+ text-transform: uppercase;
495
+ letter-spacing: 0.5px;
496
+ margin-bottom: 4px;
497
+ }
498
+
499
+ .trace-viewer__overview-value {
500
+ font-size: 13px;
501
+ font-weight: 600;
502
+ color: var(--text);
503
+ font-family: var(--mono);
504
+ }
505
+
506
+ .trace-viewer__span-list {
507
+ flex: 1;
508
+ overflow: auto;
509
+ display: flex;
510
+ flex-direction: column;
511
+ }
512
+
513
+ .trace-viewer__span-item {
514
+ padding: 8px 16px;
515
+ border-bottom: 1px solid var(--border);
516
+ cursor: pointer;
517
+ transition: background 0.2s;
518
+ }
519
+
520
+ .trace-viewer__span-item:hover {
521
+ background: var(--bg2, var(--bg));
522
+ }
523
+
524
+ .trace-viewer__span-item--selected {
525
+ background: var(--bg2, var(--bg));
526
+ border-left: 2px solid var(--accent);
527
+ }
528
+
529
+ .trace-viewer__span-name {
530
+ font-size: 12px;
531
+ color: var(--text);
532
+ font-weight: 500;
533
+ overflow: hidden;
534
+ text-overflow: ellipsis;
535
+ white-space: nowrap;
536
+ }
537
+
538
+ .trace-viewer__span-meta {
539
+ display: flex;
540
+ gap: 8px;
541
+ margin-top: 4px;
542
+ font-size: 10px;
543
+ color: var(--text2, var(--text));
544
+ }
545
+
546
+ .trace-viewer__span-type {
547
+ padding: 2px 6px;
548
+ background: var(--bg2, var(--bg));
549
+ border: 1px solid var(--border);
550
+ border-radius: 3px;
551
+ }
552
+
553
+ .trace-viewer__span-duration {
554
+ font-family: var(--mono);
555
+ }
556
+
557
+ .trace-viewer__flamegraph {
558
+ height: 100%;
559
+ overflow: hidden;
560
+ }
561
+
562
+ .trace-viewer__waterfall {
563
+ height: 100%;
564
+ overflow: hidden;
565
+ }
566
+
567
+ .trace-viewer__inspector {
568
+ width: 300px;
569
+ min-width: 300px;
570
+ border-left: 1px solid var(--border);
571
+ background: var(--bg);
572
+ overflow: auto;
573
+ }
574
+
575
+ @media (max-width: 1024px) {
576
+ .trace-viewer__container {
577
+ flex-direction: column;
578
+ gap: 0;
579
+ }
580
+
581
+ .trace-viewer__list {
582
+ width: 100%;
583
+ min-width: auto;
584
+ height: 200px;
585
+ min-height: 200px;
586
+ border-right: none;
587
+ border-bottom: 1px solid var(--border);
588
+ }
589
+
590
+ .trace-viewer__inspector {
591
+ width: 100%;
592
+ min-width: auto;
593
+ height: 200px;
594
+ min-height: 200px;
595
+ border-left: none;
596
+ border-top: 1px solid var(--border);
597
+ }
598
+ }
599
+ </style>
@@ -28,12 +28,7 @@ const filtered = computed(() => {
28
28
  list = list.filter((e) => e.phase === 'entered' || e.phase === 'left')
29
29
  }
30
30
 
31
- return list.sort((a, b) => {
32
- const aTime = a.endTime ?? a.startTime
33
- const bTime = b.endTime ?? b.startTime
34
-
35
- return bTime - aTime
36
- })
31
+ return list.sort((a, b) => a.startTime - b.startTime)
37
32
  })
38
33
 
39
34
  const stats = computed(() => ({
package/dist/module.d.mts CHANGED
@@ -71,6 +71,11 @@ interface ModuleOptions {
71
71
  * @default true
72
72
  */
73
73
  transitionTracker?: boolean;
74
+ /**
75
+ * Enable the trace viewer tab (per-route component + fetch + composable + render spans)
76
+ * @default true
77
+ */
78
+ traceViewer?: boolean;
74
79
  /**
75
80
  * Hide node_modules/internal components in the render heatmap
76
81
  * @default false
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.30",
7
+ "version": "0.1.31",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"