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.
- package/README.md +63 -4
- package/client/.env +2 -1
- package/client/.env.example +2 -1
- package/client/dist/assets/index-BuMXDBO9.js +17 -0
- package/client/dist/assets/{index-BmGW_M3W.css → index-CwcspZ6w.css} +1 -1
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +443 -0
- package/client/src/components/SpanInspector.vue +446 -0
- package/client/src/components/TraceFilter.vue +344 -0
- package/client/src/components/WaterfallView.vue +443 -0
- package/client/src/composables/useTraceFilter.ts +164 -0
- package/client/src/stores/observatory.ts +5 -1
- package/client/src/views/TraceViewer.vue +599 -0
- package/client/src/views/TransitionTimeline.vue +1 -6
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +30 -44
- package/dist/runtime/composables/render-registry.js +66 -110
- package/dist/runtime/composables/transition-registry.js +103 -28
- package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
- package/dist/runtime/instrumentation/asyncData.js +49 -0
- package/dist/runtime/instrumentation/component.d.ts +2 -0
- package/dist/runtime/instrumentation/component.js +126 -0
- package/dist/runtime/instrumentation/fetch.d.ts +2 -0
- package/dist/runtime/instrumentation/fetch.js +89 -0
- package/dist/runtime/instrumentation/route.d.ts +6 -0
- package/dist/runtime/instrumentation/route.js +41 -0
- package/dist/runtime/plugin.js +38 -2
- package/dist/runtime/tracing/context.d.ts +9 -0
- package/dist/runtime/tracing/context.js +15 -0
- package/dist/runtime/tracing/trace.d.ts +25 -0
- package/dist/runtime/tracing/trace.js +0 -0
- package/dist/runtime/tracing/traceStore.d.ts +39 -0
- package/dist/runtime/tracing/traceStore.js +101 -0
- package/dist/runtime/tracing/tracing.d.ts +27 -0
- package/dist/runtime/tracing/tracing.js +48 -0
- package/package.json +1 -1
- package/client/dist/assets/index-BCaKoHBH.js +0 -17
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
trace: TraceEntry
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const props = defineProps<Props>()
|
|
10
|
+
|
|
11
|
+
const spansByType = computed(() => {
|
|
12
|
+
const groups: Record<string, TraceSpan[]> = {}
|
|
13
|
+
|
|
14
|
+
for (const span of props.trace.spans) {
|
|
15
|
+
if (!groups[span.type]) {
|
|
16
|
+
groups[span.type] = []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
groups[span.type].push(span)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Object.entries(groups)
|
|
23
|
+
.map(([type, spans]) => ({
|
|
24
|
+
type,
|
|
25
|
+
spans: spans.sort((a, b) => a.startTime - b.startTime),
|
|
26
|
+
}))
|
|
27
|
+
.sort((a, b) => {
|
|
28
|
+
const typeOrder: Record<string, number> = {
|
|
29
|
+
navigation: 0,
|
|
30
|
+
fetch: 1,
|
|
31
|
+
composable: 2,
|
|
32
|
+
component: 3,
|
|
33
|
+
render: 4,
|
|
34
|
+
transition: 5,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (typeOrder[a.type] ?? 999) - (typeOrder[b.type] ?? 999)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const timelineDuration = computed(() => {
|
|
42
|
+
if (props.trace.durationMs && props.trace.durationMs > 0) {
|
|
43
|
+
return props.trace.durationMs
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (props.trace.endTime && props.trace.endTime > props.trace.startTime) {
|
|
47
|
+
return props.trace.endTime - props.trace.startTime
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let maxEndOffset = 0
|
|
51
|
+
|
|
52
|
+
for (const span of props.trace.spans) {
|
|
53
|
+
const endTime = span.endTime ?? span.startTime + (span.durationMs ?? 0)
|
|
54
|
+
const endOffset = endTime - props.trace.startTime
|
|
55
|
+
|
|
56
|
+
if (endOffset > maxEndOffset) {
|
|
57
|
+
maxEndOffset = endOffset
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return maxEndOffset || 1
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function getSpanX(span: TraceSpan): number {
|
|
65
|
+
const left = ((span.startTime - props.trace.startTime) / timelineDuration.value) * 100
|
|
66
|
+
|
|
67
|
+
return Math.min(100, Math.max(0, left))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSpanWidth(span: TraceSpan): number {
|
|
71
|
+
const left = getSpanX(span)
|
|
72
|
+
const width = ((span.durationMs || 0) / timelineDuration.value) * 100
|
|
73
|
+
|
|
74
|
+
return Math.max(2, Math.min(100 - left, width))
|
|
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 getSpanColorClass(type: string) {
|
|
100
|
+
const colors: Record<string, string> = {
|
|
101
|
+
fetch: 'bg-blue-500',
|
|
102
|
+
composable: 'bg-purple-500',
|
|
103
|
+
component: 'bg-green-500',
|
|
104
|
+
navigation: 'bg-yellow-500',
|
|
105
|
+
render: 'bg-orange-500',
|
|
106
|
+
transition: 'bg-pink-500',
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return colors[type] || 'bg-gray-500'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getStatusColor(status: string) {
|
|
113
|
+
const statusColors: Record<string, string> = {
|
|
114
|
+
ok: 'border-green-400',
|
|
115
|
+
error: 'border-red-400',
|
|
116
|
+
cancelled: 'border-gray-400',
|
|
117
|
+
active: 'border-yellow-400',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return statusColors[status] || 'border-gray-400'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function asString(val: unknown): string {
|
|
124
|
+
return typeof val === 'string' ? val : ''
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getSpanDisplayName(span: TraceSpan): string {
|
|
128
|
+
const m = span.metadata as Record<string, unknown> | undefined
|
|
129
|
+
|
|
130
|
+
if (!m) {
|
|
131
|
+
return span.name
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const componentName = asString(m.componentName)
|
|
135
|
+
const lifecycle = asString(m.lifecycle)
|
|
136
|
+
const uid = m.uid !== undefined ? String(m.uid) : ''
|
|
137
|
+
const method = asString(m.method)
|
|
138
|
+
const url = asString(m.url)
|
|
139
|
+
const route = asString(m.route) || asString(m.path)
|
|
140
|
+
|
|
141
|
+
if (componentName && lifecycle) {
|
|
142
|
+
return uid ? `${componentName}.${lifecycle} #${uid}` : `${componentName}.${lifecycle}`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (componentName) {
|
|
146
|
+
return uid ? `${componentName} #${uid}` : componentName
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (method && url) {
|
|
150
|
+
return `${method} ${url}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (url) {
|
|
154
|
+
return url
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (route) {
|
|
158
|
+
return `${span.name} (${route})`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return span.name
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getSpanTooltip(span: TraceSpan): string {
|
|
165
|
+
const displayName = getSpanDisplayName(span)
|
|
166
|
+
const duration = formatDuration(span.durationMs)
|
|
167
|
+
const m = span.metadata as Record<string, unknown> | undefined
|
|
168
|
+
const route = m ? asString(m.route) || asString(m.path) : ''
|
|
169
|
+
let tooltip = `${displayName} — ${duration} (${span.status})`
|
|
170
|
+
|
|
171
|
+
if (route && !displayName.includes(route)) {
|
|
172
|
+
tooltip += ` [${route}]`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return tooltip
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<div class="waterfall">
|
|
181
|
+
<div class="waterfall__timeline-header">
|
|
182
|
+
<div class="waterfall__type-col">Type</div>
|
|
183
|
+
<div class="waterfall__timeline-col">
|
|
184
|
+
<div class="waterfall__time-markers">
|
|
185
|
+
<div class="waterfall__time-marker">0ms</div>
|
|
186
|
+
<div class="waterfall__time-marker">{{ formatDuration(timelineDuration / 4) }}</div>
|
|
187
|
+
<div class="waterfall__time-marker">{{ formatDuration(timelineDuration / 2) }}</div>
|
|
188
|
+
<div class="waterfall__time-marker">{{ formatDuration((timelineDuration * 3) / 4) }}</div>
|
|
189
|
+
<div class="waterfall__time-marker">{{ formatDuration(timelineDuration) }}</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div class="waterfall__groups">
|
|
195
|
+
<div v-for="group in spansByType" :key="group.type" class="waterfall__group">
|
|
196
|
+
<div class="waterfall__group-header">
|
|
197
|
+
<span class="waterfall__group-type">
|
|
198
|
+
<span class="waterfall__color-dot" :class="getSpanColorClass(group.type)"></span>
|
|
199
|
+
{{ group.type }}
|
|
200
|
+
</span>
|
|
201
|
+
<span class="waterfall__group-count">{{ group.spans.length }}</span>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="waterfall__group-spans">
|
|
205
|
+
<div v-for="span in group.spans" :key="span.id" class="waterfall__span-row">
|
|
206
|
+
<div class="waterfall__span-label">
|
|
207
|
+
<span :title="getSpanTooltip(span)">{{ getSpanDisplayName(span) }}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="waterfall__span-bar-container">
|
|
210
|
+
<div
|
|
211
|
+
class="waterfall__span-bar"
|
|
212
|
+
:class="[getSpanColorClass(span.type), getStatusColor(span.status)]"
|
|
213
|
+
:style="{
|
|
214
|
+
left: `${getSpanX(span)}%`,
|
|
215
|
+
width: `${Math.max(1, getSpanWidth(span))}%`,
|
|
216
|
+
}"
|
|
217
|
+
:title="getSpanTooltip(span)"
|
|
218
|
+
>
|
|
219
|
+
<span v-if="!isNarrowBar(span)" class="waterfall__bar-duration">{{ formatDuration(span.durationMs) }}</span>
|
|
220
|
+
</div>
|
|
221
|
+
<span
|
|
222
|
+
v-if="isNarrowBar(span)"
|
|
223
|
+
class="waterfall__bar-duration-outside"
|
|
224
|
+
:style="{ left: `calc(${getSpanX(span)}% + 4px)` }"
|
|
225
|
+
>
|
|
226
|
+
{{ formatDuration(span.durationMs) }}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div v-if="spansByType.length === 0" class="waterfall__empty">No spans in this trace</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</template>
|
|
237
|
+
|
|
238
|
+
<style scoped>
|
|
239
|
+
.waterfall {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-direction: column;
|
|
242
|
+
height: 100%;
|
|
243
|
+
overflow: auto;
|
|
244
|
+
font-family: var(--mono);
|
|
245
|
+
font-size: 11px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.waterfall__timeline-header {
|
|
249
|
+
display: flex;
|
|
250
|
+
position: sticky;
|
|
251
|
+
top: 0;
|
|
252
|
+
z-index: 20;
|
|
253
|
+
background: var(--bg2, var(--bg));
|
|
254
|
+
box-shadow: 0 1px 0 var(--border);
|
|
255
|
+
border-bottom: 1px solid var(--border);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.waterfall__type-col {
|
|
259
|
+
width: 120px;
|
|
260
|
+
min-width: 120px;
|
|
261
|
+
padding: 8px 0;
|
|
262
|
+
border-right: 1px solid var(--border);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.waterfall__timeline-col {
|
|
266
|
+
flex: 1;
|
|
267
|
+
min-width: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.waterfall__time-markers {
|
|
271
|
+
display: flex;
|
|
272
|
+
justify-content: space-around;
|
|
273
|
+
padding: 8px 16px;
|
|
274
|
+
gap: 8px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.waterfall__time-marker {
|
|
278
|
+
font-size: 10px;
|
|
279
|
+
color: var(--text2, var(--text));
|
|
280
|
+
flex: 1;
|
|
281
|
+
text-align: center;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.waterfall__groups {
|
|
285
|
+
display: flex;
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.waterfall__group {
|
|
290
|
+
border-bottom: 1px solid var(--border);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.waterfall__group-header {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
background: var(--bg2, var(--bg));
|
|
298
|
+
padding: 8px 16px;
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
font-size: 12px;
|
|
301
|
+
color: var(--text2, var(--text));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.waterfall__group-type {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: 6px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.waterfall__color-dot {
|
|
311
|
+
display: inline-block;
|
|
312
|
+
width: 8px;
|
|
313
|
+
height: 8px;
|
|
314
|
+
border-radius: 2px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.waterfall__group-count {
|
|
318
|
+
background: var(--bg);
|
|
319
|
+
border: 1px solid var(--border);
|
|
320
|
+
padding: 2px 6px;
|
|
321
|
+
border-radius: 3px;
|
|
322
|
+
font-size: 10px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.waterfall__group-spans {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.waterfall__span-row {
|
|
331
|
+
display: flex;
|
|
332
|
+
height: 28px;
|
|
333
|
+
align-items: center;
|
|
334
|
+
border-bottom: 1px solid var(--border);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.waterfall__span-label {
|
|
338
|
+
width: 120px;
|
|
339
|
+
min-width: 120px;
|
|
340
|
+
padding: 0 16px;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
text-overflow: ellipsis;
|
|
343
|
+
white-space: nowrap;
|
|
344
|
+
color: var(--text);
|
|
345
|
+
font-size: 11px;
|
|
346
|
+
border-right: 1px solid var(--border);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.waterfall__span-bar-container {
|
|
350
|
+
flex: 1;
|
|
351
|
+
height: 100%;
|
|
352
|
+
position: relative;
|
|
353
|
+
min-width: 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.waterfall__span-bar {
|
|
357
|
+
position: absolute;
|
|
358
|
+
top: 50%;
|
|
359
|
+
transform: translateY(-50%);
|
|
360
|
+
height: 16px;
|
|
361
|
+
border-radius: 2px;
|
|
362
|
+
border-left: 2px solid;
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
justify-content: center;
|
|
366
|
+
color: white;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
transition: opacity 0.2s;
|
|
370
|
+
padding: 0 4px;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.waterfall__span-bar:hover {
|
|
374
|
+
opacity: 0.8;
|
|
375
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.waterfall__bar-duration {
|
|
379
|
+
font-size: 9px;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.waterfall__bar-duration-outside {
|
|
384
|
+
position: absolute;
|
|
385
|
+
top: 50%;
|
|
386
|
+
transform: translateY(-50%);
|
|
387
|
+
font-size: 9px;
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
color: var(--text);
|
|
390
|
+
pointer-events: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.waterfall__empty {
|
|
394
|
+
padding: 32px 16px;
|
|
395
|
+
text-align: center;
|
|
396
|
+
color: var(--text2, var(--text));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Color utilities */
|
|
400
|
+
.bg-blue-500 {
|
|
401
|
+
background-color: #3b82f6;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.bg-purple-500 {
|
|
405
|
+
background-color: #a855f7;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.bg-green-500 {
|
|
409
|
+
background-color: #22c55e;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.bg-yellow-500 {
|
|
413
|
+
background-color: #eab308;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.bg-orange-500 {
|
|
417
|
+
background-color: #f97316;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.bg-pink-500 {
|
|
421
|
+
background-color: #ec4899;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.bg-gray-500 {
|
|
425
|
+
background-color: #6b7280;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.border-green-400 {
|
|
429
|
+
border-color: #4ade80;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.border-red-400 {
|
|
433
|
+
border-color: #f87171;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.border-gray-400 {
|
|
437
|
+
border-color: #9ca3af;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.border-yellow-400 {
|
|
441
|
+
border-color: #facc15;
|
|
442
|
+
}
|
|
443
|
+
</style>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import type { TraceEntry } from '@observatory/types/snapshot'
|
|
3
|
+
|
|
4
|
+
export function getSpanTypesFromTraces(traces: TraceEntry[]): string[] {
|
|
5
|
+
const types = new Set<string>()
|
|
6
|
+
|
|
7
|
+
for (const trace of traces) {
|
|
8
|
+
for (const span of trace.spans) {
|
|
9
|
+
types.add(span.type)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Array.from(types).sort()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useTraceFilter() {
|
|
17
|
+
const searchQuery = ref<string>('')
|
|
18
|
+
const selectedSpanTypes = ref<Set<string>>(new Set())
|
|
19
|
+
const minDuration = ref<number>(0)
|
|
20
|
+
const maxDuration = ref<number>(Infinity)
|
|
21
|
+
const routeFilter = ref<string>('')
|
|
22
|
+
|
|
23
|
+
function matchesSearch(trace: TraceEntry, query: string): boolean {
|
|
24
|
+
if (!query) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lowerQuery = query.toLowerCase()
|
|
29
|
+
|
|
30
|
+
// Search in trace name
|
|
31
|
+
if (trace.name.toLowerCase().includes(lowerQuery)) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Search in span names
|
|
36
|
+
for (const span of trace.spans) {
|
|
37
|
+
if (span.name.toLowerCase().includes(lowerQuery)) {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Search in metadata (e.g., route, URL endpoint)
|
|
43
|
+
for (const span of trace.spans) {
|
|
44
|
+
if (span.metadata) {
|
|
45
|
+
for (const value of Object.values(span.metadata)) {
|
|
46
|
+
if (typeof value === 'string' && value.toLowerCase().includes(lowerQuery)) {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesSpanTypeFilter(trace: TraceEntry, types: Set<string>): boolean {
|
|
57
|
+
if (types.size === 0) return true
|
|
58
|
+
|
|
59
|
+
for (const span of trace.spans) {
|
|
60
|
+
if (types.has(span.type)) {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matchesDurationFilter(
|
|
69
|
+
trace: TraceEntry,
|
|
70
|
+
min: number,
|
|
71
|
+
max: number
|
|
72
|
+
): boolean {
|
|
73
|
+
const hasExplicitDurationFilter = min > 0 || max < Infinity
|
|
74
|
+
|
|
75
|
+
if (trace.durationMs === undefined) {
|
|
76
|
+
return !hasExplicitDurationFilter
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return trace.durationMs >= min && trace.durationMs <= max
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchesRouteFilter(trace: TraceEntry, route: string): boolean {
|
|
83
|
+
if (!route) {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check trace metadata for route
|
|
88
|
+
if (trace.metadata?.route && typeof trace.metadata.route === 'string') {
|
|
89
|
+
if (trace.metadata.route.toLowerCase().includes(route.toLowerCase())) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check span metadata for route or path
|
|
95
|
+
for (const span of trace.spans) {
|
|
96
|
+
if (span.metadata?.route) {
|
|
97
|
+
const routeValue = String(span.metadata.route).toLowerCase()
|
|
98
|
+
|
|
99
|
+
if (routeValue.includes(route.toLowerCase())) {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (span.metadata?.path) {
|
|
105
|
+
const pathValue = String(span.metadata.path).toLowerCase()
|
|
106
|
+
|
|
107
|
+
if (pathValue.includes(route.toLowerCase())) {
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function filterTraces(traces: TraceEntry[]): TraceEntry[] {
|
|
117
|
+
return traces.filter((trace) => {
|
|
118
|
+
return (
|
|
119
|
+
matchesSearch(trace, searchQuery.value) &&
|
|
120
|
+
matchesSpanTypeFilter(trace, selectedSpanTypes.value) &&
|
|
121
|
+
matchesDurationFilter(trace, minDuration.value, maxDuration.value) &&
|
|
122
|
+
matchesRouteFilter(trace, routeFilter.value)
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toggleSpanType(type: string) {
|
|
128
|
+
if (selectedSpanTypes.value.has(type)) {
|
|
129
|
+
selectedSpanTypes.value.delete(type)
|
|
130
|
+
} else {
|
|
131
|
+
selectedSpanTypes.value.add(type)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clearFilters() {
|
|
136
|
+
searchQuery.value = ''
|
|
137
|
+
selectedSpanTypes.value.clear()
|
|
138
|
+
minDuration.value = 0
|
|
139
|
+
maxDuration.value = Infinity
|
|
140
|
+
routeFilter.value = ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const hasActiveFilters = computed(() => {
|
|
144
|
+
return (
|
|
145
|
+
searchQuery.value.length > 0 ||
|
|
146
|
+
selectedSpanTypes.value.size > 0 ||
|
|
147
|
+
minDuration.value > 0 ||
|
|
148
|
+
maxDuration.value < Infinity ||
|
|
149
|
+
routeFilter.value.length > 0
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
searchQuery,
|
|
155
|
+
selectedSpanTypes,
|
|
156
|
+
minDuration,
|
|
157
|
+
maxDuration,
|
|
158
|
+
routeFilter,
|
|
159
|
+
filterTraces,
|
|
160
|
+
toggleSpanType,
|
|
161
|
+
clearFilters,
|
|
162
|
+
hasActiveFilters,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ref } from 'vue'
|
|
2
2
|
import { useDevtoolsClient, onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
|
|
3
3
|
import type { ObservatorySnapshot, ObservatoryServerFunctions, ObservatoryClientFunctions } from '@observatory/types/rpc'
|
|
4
|
-
import type { FetchEntry, ProvideEntry, InjectEntry, ComposableEntry, RenderEntry, TransitionEntry } from '@observatory/types/snapshot'
|
|
4
|
+
import type { FetchEntry, ProvideEntry, InjectEntry, ComposableEntry, RenderEntry, TransitionEntry, TraceEntry } from '@observatory/types/snapshot'
|
|
5
5
|
|
|
6
6
|
type ProvideInjectSnapshot = { provides: ProvideEntry[]; injects: InjectEntry[] }
|
|
7
7
|
|
|
@@ -10,6 +10,7 @@ const provideInject = ref<ProvideInjectSnapshot>({ provides: [], injects: [] })
|
|
|
10
10
|
const composables = ref<ComposableEntry[]>([])
|
|
11
11
|
const renders = ref<RenderEntry[]>([])
|
|
12
12
|
const transitions = ref<TransitionEntry[]>([])
|
|
13
|
+
const traces = ref<TraceEntry[]>([])
|
|
13
14
|
const connected = ref(false)
|
|
14
15
|
const features = ref<ObservatorySnapshot['features']>({})
|
|
15
16
|
const debugRpc = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugRpc')
|
|
@@ -50,6 +51,7 @@ function applySnapshot(data: ObservatorySnapshot) {
|
|
|
50
51
|
composables.value = cloneArray(data.composables as ComposableEntry[] | undefined)
|
|
51
52
|
renders.value = normalizeRenderEntries(data.renders as RenderEntry[] | undefined)
|
|
52
53
|
transitions.value = cloneArray(data.transitions as TransitionEntry[] | undefined)
|
|
54
|
+
traces.value = cloneArray(data.traces as TraceEntry[] | undefined)
|
|
53
55
|
features.value = data.features || {}
|
|
54
56
|
|
|
55
57
|
// If the server snapshot disagrees with the user's requested mode,
|
|
@@ -79,6 +81,7 @@ function applySnapshot(data: ObservatorySnapshot) {
|
|
|
79
81
|
composables: composables.value.length,
|
|
80
82
|
renders: renders.value.length,
|
|
81
83
|
transitions: transitions.value.length,
|
|
84
|
+
traces: traces.value.length,
|
|
82
85
|
})
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -224,6 +227,7 @@ export function useObservatoryData() {
|
|
|
224
227
|
composables,
|
|
225
228
|
renders,
|
|
226
229
|
transitions,
|
|
230
|
+
traces,
|
|
227
231
|
features,
|
|
228
232
|
connected,
|
|
229
233
|
refresh,
|