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,443 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
4
+
5
+ interface TraceNode extends TraceSpan {
6
+ children: TraceNode[]
7
+ level: number
8
+ }
9
+
10
+ interface Props {
11
+ trace: TraceEntry
12
+ selectedSpanId?: string
13
+ }
14
+
15
+ const props = defineProps<Props>()
16
+ const emit = defineEmits<{
17
+ 'select-span': [span: TraceSpan]
18
+ }>()
19
+
20
+ const expandedNodes = ref<Set<string>>(new Set())
21
+
22
+ function toggleExpanded(spanId: string) {
23
+ if (expandedNodes.value.has(spanId)) {
24
+ expandedNodes.value.delete(spanId)
25
+ } else {
26
+ expandedNodes.value.add(spanId)
27
+ }
28
+ }
29
+
30
+ function buildTree(spans: TraceSpan[], parentId: string | undefined = undefined, level = 0): TraceNode[] {
31
+ return spans
32
+ .filter((s) => s.parentSpanId === parentId)
33
+ .map((span) => ({
34
+ ...span,
35
+ children: buildTree(spans, span.id, level + 1),
36
+ level,
37
+ }))
38
+ }
39
+
40
+ const spanTree = computed(() => buildTree(props.trace.spans, undefined, 0))
41
+
42
+ const timelineDuration = computed(() => {
43
+ if (props.trace.durationMs && props.trace.durationMs > 0) {
44
+ return props.trace.durationMs
45
+ }
46
+
47
+ if (props.trace.endTime && props.trace.endTime > props.trace.startTime) {
48
+ return props.trace.endTime - props.trace.startTime
49
+ }
50
+
51
+ let maxEndOffset = 0
52
+
53
+ for (const span of props.trace.spans) {
54
+ const endTime = span.endTime ?? span.startTime + (span.durationMs ?? 0)
55
+ const endOffset = endTime - props.trace.startTime
56
+
57
+ if (endOffset > maxEndOffset) {
58
+ maxEndOffset = endOffset
59
+ }
60
+ }
61
+
62
+ // Add 10% padding so the rightmost span isn't clipped at 100%
63
+ return maxEndOffset > 0 ? maxEndOffset * 1.1 : 1
64
+ })
65
+
66
+ function getBarPosition(span: TraceSpan): { left: string; width: string } {
67
+ const traceStart = props.trace.startTime
68
+ const left = ((span.startTime - traceStart) / timelineDuration.value) * 100
69
+ const width = ((span.durationMs || 0) / timelineDuration.value) * 100
70
+
71
+ return {
72
+ left: `${Math.min(100, Math.max(0, left))}%`,
73
+ width: `${Math.max(0.5, Math.min(100 - Math.max(0, left), width))}%`,
74
+ }
75
+ }
76
+
77
+ function isNarrowBar(span: TraceSpan): boolean {
78
+ const width = ((span.durationMs || 0) / timelineDuration.value) * 100
79
+
80
+ return width < 5
81
+ }
82
+
83
+ function formatDuration(durationMs?: number) {
84
+ if (!durationMs) {
85
+ return '0ms'
86
+ }
87
+
88
+ if (durationMs < 1) {
89
+ return `${(durationMs * 1000).toFixed(1)}μs`
90
+ }
91
+
92
+ if (durationMs < 1000) {
93
+ return `${durationMs.toFixed(1)}ms`
94
+ }
95
+
96
+ return `${(durationMs / 1000).toFixed(2)}s`
97
+ }
98
+
99
+ function asString(value: unknown): string | undefined {
100
+ return typeof value === 'string' && value.length > 0 ? value : undefined
101
+ }
102
+
103
+ function asNumber(value: unknown): number | undefined {
104
+ return typeof value === 'number' ? value : undefined
105
+ }
106
+
107
+ function getSpanDisplayName(span: TraceSpan): string {
108
+ const metadata = span.metadata ?? {}
109
+ const lifecycle = asString(metadata.lifecycle)
110
+ const componentName = asString(metadata.componentName)
111
+ const uid = asNumber(metadata.uid)
112
+ const route = asString(metadata.route) ?? asString(metadata.path)
113
+ const method = asString(metadata.method)
114
+ const url = asString(metadata.url)
115
+
116
+ if (componentName && lifecycle) {
117
+ return uid !== undefined ? `${componentName}.${lifecycle} #${uid}` : `${componentName}.${lifecycle}`
118
+ }
119
+
120
+ if (componentName) {
121
+ return uid !== undefined ? `${componentName} #${uid}` : componentName
122
+ }
123
+
124
+ if (method && url) {
125
+ return `${method} ${url}`
126
+ }
127
+
128
+ if (url) {
129
+ return url
130
+ }
131
+
132
+ if (route) {
133
+ return `${span.name} (${route})`
134
+ }
135
+
136
+ return span.name
137
+ }
138
+
139
+ function getSpanTooltip(span: TraceSpan): string {
140
+ const metadata = span.metadata ?? {}
141
+ const displayName = getSpanDisplayName(span)
142
+ const status = span.status
143
+ const duration = formatDuration(span.durationMs)
144
+ const route = asString(metadata.route) ?? asString(metadata.path)
145
+
146
+ if (route) {
147
+ return `${displayName} - ${duration} (${status}) [${route}]`
148
+ }
149
+
150
+ return `${displayName} - ${duration} (${status})`
151
+ }
152
+
153
+ function getSpanColorClass(type: string) {
154
+ const colors: Record<string, string> = {
155
+ fetch: 'bg-blue-500',
156
+ composable: 'bg-purple-500',
157
+ component: 'bg-green-500',
158
+ navigation: 'bg-yellow-500',
159
+ render: 'bg-orange-500',
160
+ transition: 'bg-pink-500',
161
+ }
162
+
163
+ return colors[type] || 'bg-gray-500'
164
+ }
165
+
166
+ function renderNode(node: TraceNode): TraceNode[] {
167
+ const result: TraceNode[] = [node]
168
+
169
+ if (expandedNodes.value.has(node.id) && node.children.length > 0) {
170
+ for (const child of node.children) {
171
+ result.push(...renderNode(child))
172
+ }
173
+ }
174
+
175
+ return result
176
+ }
177
+
178
+ const flattenedTree = computed(() => {
179
+ const result: Array<{ node: TraceNode; displayDepth: number }> = []
180
+
181
+ for (const rootNode of spanTree.value) {
182
+ for (const node of renderNode(rootNode)) {
183
+ const depth = node.level + (node.parentSpanId ? 1 : 0)
184
+ result.push({ node, displayDepth: depth })
185
+ }
186
+ }
187
+
188
+ return result
189
+ })
190
+ </script>
191
+
192
+ <template>
193
+ <div class="flamegraph">
194
+ <div class="flamegraph__timeline-header">
195
+ <div class="flamegraph__time-label">0ms</div>
196
+ <div class="flamegraph__time-label">{{ formatDuration(timelineDuration / 2) }}</div>
197
+ <div class="flamegraph__time-label">{{ formatDuration(timelineDuration) }}</div>
198
+ </div>
199
+
200
+ <div class="flamegraph__rows">
201
+ <div
202
+ v-for="{ node, displayDepth } in flattenedTree"
203
+ :key="node.id"
204
+ :class="{ 'flamegraph__row--selected': node.id === props.selectedSpanId }"
205
+ class="flamegraph__row"
206
+ >
207
+ <div class="flamegraph__label" :style="{ paddingLeft: `calc(8px + ${displayDepth * 16}px)` }">
208
+ <button
209
+ v-if="node.children.length > 0"
210
+ :class="{
211
+ 'flamegraph__expand-btn': true,
212
+ 'flamegraph__expand-btn--expanded': expandedNodes.has(node.id),
213
+ }"
214
+ @click="toggleExpanded(node.id)"
215
+ >
216
+
217
+ </button>
218
+ <button class="flamegraph__node-button" @click="emit('select-span', node)">
219
+ <span class="flamegraph__span-name">{{ getSpanDisplayName(node) }}</span>
220
+ <span class="flamegraph__span-type">{{ node.type }}</span>
221
+ </button>
222
+ </div>
223
+
224
+ <div class="flamegraph__bar-container">
225
+ <div
226
+ class="flamegraph__bar"
227
+ :class="getSpanColorClass(node.type)"
228
+ :style="getBarPosition(node)"
229
+ :title="getSpanTooltip(node)"
230
+ @click="emit('select-span', node)"
231
+ >
232
+ <span v-if="!isNarrowBar(node)" class="flamegraph__bar-label">{{ formatDuration(node.durationMs) }}</span>
233
+ </div>
234
+ <span v-if="isNarrowBar(node)" class="flamegraph__bar-label-outside" :style="{ left: getBarPosition(node).left }">
235
+ {{ formatDuration(node.durationMs) }}
236
+ </span>
237
+ </div>
238
+ </div>
239
+
240
+ <div v-if="flattenedTree.length === 0" class="flamegraph__empty">No spans in this trace</div>
241
+ </div>
242
+ </div>
243
+ </template>
244
+
245
+ <style scoped>
246
+ .flamegraph {
247
+ display: flex;
248
+ flex-direction: column;
249
+ height: 100%;
250
+ min-width: 0;
251
+ overflow-x: hidden;
252
+ overflow-y: auto;
253
+ font-family: var(--mono);
254
+ font-size: 11px;
255
+ }
256
+
257
+ .flamegraph__timeline-header {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ padding: 8px 0;
261
+ border-bottom: 1px solid var(--border);
262
+ position: sticky;
263
+ top: 0;
264
+ min-width: 0;
265
+ background: var(--bg2, var(--bg));
266
+ box-shadow: 0 1px 0 var(--border);
267
+ z-index: 10;
268
+ }
269
+
270
+ .flamegraph__time-label {
271
+ font-size: 10px;
272
+ color: var(--text2, var(--text));
273
+ padding: 0 8px;
274
+ }
275
+
276
+ .flamegraph__rows {
277
+ display: flex;
278
+ flex-direction: column;
279
+ min-width: 0;
280
+ }
281
+
282
+ .flamegraph__row {
283
+ display: flex;
284
+ height: 28px;
285
+ border-bottom: 1px solid var(--border);
286
+ align-items: center;
287
+ min-width: 0;
288
+ cursor: pointer;
289
+ }
290
+
291
+ .flamegraph__row--selected {
292
+ background: var(--tracker-tint-purple-soft, rgba(127, 119, 221, 0.08));
293
+ }
294
+
295
+ .flamegraph__row--selected .flamegraph__span-name {
296
+ color: var(--purple, var(--accent));
297
+ }
298
+
299
+ .flamegraph__label {
300
+ display: flex;
301
+ align-items: center;
302
+ width: 280px;
303
+ min-width: 280px;
304
+ gap: 6px;
305
+ padding-right: 8px;
306
+ overflow: hidden;
307
+ }
308
+
309
+ .flamegraph__node-button {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: 6px;
313
+ min-width: 0;
314
+ padding: 0;
315
+ border: none;
316
+ background: transparent;
317
+ color: inherit;
318
+ cursor: pointer;
319
+ text-align: left;
320
+ }
321
+
322
+ .flamegraph__expand-btn {
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+ width: 16px;
327
+ height: 16px;
328
+ padding: 0;
329
+ margin: 0;
330
+ background: none;
331
+ border: none;
332
+ color: var(--text2, var(--text));
333
+ cursor: pointer;
334
+ font-size: 10px;
335
+ transition: transform 0.2s;
336
+ }
337
+
338
+ .flamegraph__expand-btn--expanded {
339
+ transform: rotate(90deg);
340
+ }
341
+
342
+ .flamegraph__expand-btn:hover {
343
+ color: var(--text);
344
+ }
345
+
346
+ .flamegraph__span-name {
347
+ flex: 1;
348
+ overflow: hidden;
349
+ text-overflow: ellipsis;
350
+ white-space: nowrap;
351
+ color: var(--text);
352
+ }
353
+
354
+ .flamegraph__span-type {
355
+ padding: 2px 6px;
356
+ background: var(--bg2, var(--bg));
357
+ border-radius: 3px;
358
+ color: var(--text2, var(--text));
359
+ font-size: 9px;
360
+ flex-shrink: 0;
361
+ }
362
+
363
+ .flamegraph__bar-container {
364
+ flex: 1;
365
+ height: 100%;
366
+ min-width: 0;
367
+ position: relative;
368
+ padding: 4px 0;
369
+ overflow: hidden;
370
+ }
371
+
372
+ .flamegraph__bar {
373
+ position: absolute;
374
+ top: 50%;
375
+ transform: translateY(-50%);
376
+ height: 16px;
377
+ border-radius: 2px;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ color: white;
382
+ overflow: hidden;
383
+ cursor: pointer;
384
+ transition: opacity 0.2s;
385
+ }
386
+
387
+ .flamegraph__bar:hover {
388
+ opacity: 0.8;
389
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
390
+ }
391
+
392
+ .flamegraph__bar-label {
393
+ font-size: 9px;
394
+ white-space: nowrap;
395
+ padding: 0 4px;
396
+ }
397
+
398
+ .flamegraph__bar-label-outside {
399
+ position: absolute;
400
+ top: 50%;
401
+ transform: translateY(-50%);
402
+ font-size: 9px;
403
+ white-space: nowrap;
404
+ color: var(--text);
405
+ padding-left: 4px;
406
+ pointer-events: none;
407
+ }
408
+
409
+ .flamegraph__empty {
410
+ padding: 32px 16px;
411
+ text-align: center;
412
+ color: var(--text2, var(--text));
413
+ }
414
+
415
+ /* Color utilities */
416
+ .bg-blue-500 {
417
+ background-color: #3b82f6;
418
+ }
419
+
420
+ .bg-purple-500 {
421
+ background-color: #a855f7;
422
+ }
423
+
424
+ .bg-green-500 {
425
+ background-color: #22c55e;
426
+ }
427
+
428
+ .bg-yellow-500 {
429
+ background-color: #eab308;
430
+ }
431
+
432
+ .bg-orange-500 {
433
+ background-color: #f97316;
434
+ }
435
+
436
+ .bg-pink-500 {
437
+ background-color: #ec4899;
438
+ }
439
+
440
+ .bg-gray-500 {
441
+ background-color: #6b7280;
442
+ }
443
+ </style>