nuxt-devtools-observatory 0.1.5 → 0.1.7
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/client/dist/assets/index-CgZM5MBX.js +17 -0
- package/client/dist/assets/{index-yIuOV1_N.css → index-mu192QeW.css} +1 -1
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +154 -34
- package/client/src/views/ComponentBlock.vue +162 -0
- package/client/src/views/ComposableTracker.vue +103 -138
- package/client/src/views/FetchDashboard.vue +101 -199
- package/client/src/views/ProvideInjectGraph.vue +159 -136
- package/client/src/views/RenderHeatmap.vue +107 -151
- package/client/src/views/TransitionTimeline.vue +9 -3
- package/dist/module.d.mts +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +89 -24
- package/dist/runtime/composables/composable-registry.js +42 -1
- package/dist/runtime/composables/fetch-registry.d.ts +2 -2
- package/dist/runtime/composables/fetch-registry.js +120 -43
- package/dist/runtime/composables/provide-inject-registry.d.ts +2 -19
- package/dist/runtime/composables/provide-inject-registry.js +43 -1
- package/dist/runtime/composables/render-registry.js +71 -9
- package/dist/runtime/composables/transition-registry.js +10 -1
- package/dist/runtime/plugin.js +25 -10
- package/package.json +6 -4
- package/client/dist/assets/index-C76d764s.js +0 -17
|
@@ -1,219 +1,118 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
+
import { useObservatoryData, type FetchEntry } from '../stores/observatory'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
id: string
|
|
6
|
-
key: string
|
|
7
|
-
url: string
|
|
8
|
-
status: 'pending' | 'ok' | 'error' | 'cached'
|
|
9
|
-
origin: 'ssr' | 'csr'
|
|
10
|
-
ms?: number
|
|
11
|
-
size?: number
|
|
12
|
-
cached: boolean
|
|
13
|
-
payload?: unknown
|
|
14
|
-
file?: string
|
|
15
|
-
line?: number
|
|
16
|
-
startOffset?: number
|
|
17
|
-
}
|
|
5
|
+
type FetchViewEntry = FetchEntry & { startOffset: number }
|
|
18
6
|
|
|
19
|
-
|
|
20
|
-
const entries = ref<FetchEntry[]>([
|
|
21
|
-
{
|
|
22
|
-
id: '1',
|
|
23
|
-
key: 'product-detail',
|
|
24
|
-
url: '/api/products/42',
|
|
25
|
-
status: 'ok',
|
|
26
|
-
origin: 'ssr',
|
|
27
|
-
ms: 48,
|
|
28
|
-
size: 3276,
|
|
29
|
-
cached: false,
|
|
30
|
-
startOffset: 0,
|
|
31
|
-
file: 'pages/products/[id].vue',
|
|
32
|
-
line: 8,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: '2',
|
|
36
|
-
key: 'related-products',
|
|
37
|
-
url: '/api/products?related=42',
|
|
38
|
-
status: 'ok',
|
|
39
|
-
origin: 'ssr',
|
|
40
|
-
ms: 112,
|
|
41
|
-
size: 19148,
|
|
42
|
-
cached: false,
|
|
43
|
-
startOffset: 10,
|
|
44
|
-
file: 'pages/products/[id].vue',
|
|
45
|
-
line: 14,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: '3',
|
|
49
|
-
key: 'user-session',
|
|
50
|
-
url: '/api/auth/session',
|
|
51
|
-
status: 'cached',
|
|
52
|
-
origin: 'csr',
|
|
53
|
-
ms: 0,
|
|
54
|
-
size: 819,
|
|
55
|
-
cached: true,
|
|
56
|
-
startOffset: 0,
|
|
57
|
-
file: 'layouts/default.vue',
|
|
58
|
-
line: 5,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: '4',
|
|
62
|
-
key: 'cart-summary',
|
|
63
|
-
url: '/api/cart',
|
|
64
|
-
status: 'ok',
|
|
65
|
-
origin: 'csr',
|
|
66
|
-
ms: 67,
|
|
67
|
-
size: 2150,
|
|
68
|
-
cached: false,
|
|
69
|
-
startOffset: 5,
|
|
70
|
-
file: 'components/CartDrawer.vue',
|
|
71
|
-
line: 3,
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: '5',
|
|
75
|
-
key: 'product-reviews',
|
|
76
|
-
url: '/api/products/42/reviews',
|
|
77
|
-
status: 'pending',
|
|
78
|
-
origin: 'csr',
|
|
79
|
-
ms: undefined,
|
|
80
|
-
size: undefined,
|
|
81
|
-
cached: false,
|
|
82
|
-
startOffset: 120,
|
|
83
|
-
file: 'components/ReviewList.vue',
|
|
84
|
-
line: 6,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: '6',
|
|
88
|
-
key: 'recommendations',
|
|
89
|
-
url: '/api/recommend?u=u_9x3k',
|
|
90
|
-
status: 'ok',
|
|
91
|
-
origin: 'csr',
|
|
92
|
-
ms: 201,
|
|
93
|
-
size: 9626,
|
|
94
|
-
cached: false,
|
|
95
|
-
startOffset: 30,
|
|
96
|
-
file: 'components/Recommendations.vue',
|
|
97
|
-
line: 4,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: '7',
|
|
101
|
-
key: 'inventory-check',
|
|
102
|
-
url: '/api/inventory/42',
|
|
103
|
-
status: 'error',
|
|
104
|
-
origin: 'csr',
|
|
105
|
-
ms: 503,
|
|
106
|
-
size: undefined,
|
|
107
|
-
cached: false,
|
|
108
|
-
startOffset: 55,
|
|
109
|
-
file: 'components/StockBadge.vue',
|
|
110
|
-
line: 7,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: '8',
|
|
114
|
-
key: 'nav-links',
|
|
115
|
-
url: '/api/nav',
|
|
116
|
-
status: 'cached',
|
|
117
|
-
origin: 'ssr',
|
|
118
|
-
ms: 0,
|
|
119
|
-
size: 1126,
|
|
120
|
-
cached: true,
|
|
121
|
-
startOffset: 0,
|
|
122
|
-
file: 'layouts/default.vue',
|
|
123
|
-
line: 9,
|
|
124
|
-
},
|
|
125
|
-
])
|
|
7
|
+
const { fetch, connected } = useObservatoryData()
|
|
126
8
|
|
|
127
9
|
const filter = ref<string>('all')
|
|
128
10
|
const search = ref('')
|
|
129
|
-
const
|
|
11
|
+
const selectedId = ref<string | null>(null)
|
|
12
|
+
|
|
13
|
+
const entries = computed<FetchViewEntry[]>(() => {
|
|
14
|
+
const sorted = [...fetch.value].sort((a, b) => a.startTime - b.startTime)
|
|
15
|
+
const minStart = Math.min(...sorted.map((entry) => entry.startTime), 0)
|
|
16
|
+
|
|
17
|
+
return sorted.map((entry) => ({
|
|
18
|
+
...entry,
|
|
19
|
+
startOffset: Math.max(0, Math.round(entry.startTime - minStart)),
|
|
20
|
+
}))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const selected = computed(() => entries.value.find((entry) => entry.id === selectedId.value) ?? null)
|
|
130
24
|
|
|
131
25
|
const counts = computed(() => ({
|
|
132
|
-
ok: entries.value.filter((
|
|
133
|
-
pending: entries.value.filter((
|
|
134
|
-
error: entries.value.filter((
|
|
26
|
+
ok: entries.value.filter((entry) => entry.status === 'ok').length,
|
|
27
|
+
pending: entries.value.filter((entry) => entry.status === 'pending').length,
|
|
28
|
+
error: entries.value.filter((entry) => entry.status === 'error').length,
|
|
135
29
|
}))
|
|
136
30
|
|
|
137
31
|
const filtered = computed(() => {
|
|
138
|
-
return entries.value.filter((
|
|
139
|
-
if (filter.value !== 'all' &&
|
|
32
|
+
return entries.value.filter((entry) => {
|
|
33
|
+
if (filter.value !== 'all' && entry.status !== filter.value) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
140
37
|
const q = search.value.toLowerCase()
|
|
141
|
-
|
|
38
|
+
|
|
39
|
+
if (q && !entry.key.toLowerCase().includes(q) && !entry.url.toLowerCase().includes(q)) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
142
43
|
return true
|
|
143
44
|
})
|
|
144
45
|
})
|
|
145
46
|
|
|
146
47
|
const metaRows = computed(() => {
|
|
147
|
-
if (!selected.value)
|
|
148
|
-
|
|
48
|
+
if (!selected.value) {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = selected.value
|
|
53
|
+
|
|
149
54
|
return [
|
|
150
|
-
['url',
|
|
151
|
-
['status',
|
|
152
|
-
['origin',
|
|
153
|
-
['duration',
|
|
154
|
-
['size',
|
|
155
|
-
['cached',
|
|
55
|
+
['url', entry.url],
|
|
56
|
+
['status', entry.status],
|
|
57
|
+
['origin', entry.origin],
|
|
58
|
+
['duration', entry.ms != null ? `${entry.ms}ms` : '—'],
|
|
59
|
+
['size', entry.size ? formatSize(entry.size) : '—'],
|
|
60
|
+
['cached', entry.cached ? 'yes' : 'no'],
|
|
156
61
|
]
|
|
157
62
|
})
|
|
158
63
|
|
|
159
64
|
const payloadStr = computed(() => {
|
|
160
|
-
if (!selected.value)
|
|
161
|
-
|
|
162
|
-
|
|
65
|
+
if (!selected.value) {
|
|
66
|
+
return ''
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = selected.value.payload
|
|
70
|
+
|
|
71
|
+
if (payload === undefined) {
|
|
72
|
+
return '(no payload captured yet)'
|
|
73
|
+
}
|
|
74
|
+
|
|
163
75
|
try {
|
|
164
|
-
return JSON.stringify(
|
|
76
|
+
return JSON.stringify(payload, null, 2)
|
|
165
77
|
} catch {
|
|
166
|
-
return String(
|
|
78
|
+
return String(payload)
|
|
167
79
|
}
|
|
168
80
|
})
|
|
169
81
|
|
|
170
|
-
function statusClass(
|
|
171
|
-
return { ok: 'badge-ok', error: 'badge-err', pending: 'badge-warn', cached: 'badge-gray' }[
|
|
82
|
+
function statusClass(status: string) {
|
|
83
|
+
return { ok: 'badge-ok', error: 'badge-err', pending: 'badge-warn', cached: 'badge-gray' }[status] ?? 'badge-gray'
|
|
172
84
|
}
|
|
173
85
|
|
|
174
|
-
function barColor(
|
|
175
|
-
return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[
|
|
86
|
+
function barColor(status: string) {
|
|
87
|
+
return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[status] ?? 'var(--border)'
|
|
176
88
|
}
|
|
177
89
|
|
|
178
|
-
function barWidth(
|
|
179
|
-
const maxMs = Math.max(...entries.value.filter((
|
|
180
|
-
return
|
|
90
|
+
function barWidth(entry: FetchViewEntry) {
|
|
91
|
+
const maxMs = Math.max(...entries.value.filter((item) => item.ms).map((item) => item.ms!), 1)
|
|
92
|
+
return entry.ms != null ? Math.max(4, Math.round((entry.ms / maxMs) * 100)) : 4
|
|
181
93
|
}
|
|
182
94
|
|
|
183
|
-
function wfLeft(
|
|
184
|
-
const maxEnd = Math.max(...entries.value.map((
|
|
185
|
-
return Math.round((
|
|
186
|
-
}
|
|
187
|
-
function wfWidth(e: FetchEntry) {
|
|
188
|
-
const maxEnd = Math.max(...entries.value.map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
189
|
-
return e.ms != null ? Math.round((e.ms / maxEnd) * 100) : 2
|
|
95
|
+
function wfLeft(entry: FetchViewEntry) {
|
|
96
|
+
const maxEnd = Math.max(...entries.value.map((item) => item.startOffset + (item.ms ?? 0)), 1)
|
|
97
|
+
return Math.round((entry.startOffset / maxEnd) * 100)
|
|
190
98
|
}
|
|
191
99
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
return (
|
|
100
|
+
function wfWidth(entry: FetchViewEntry) {
|
|
101
|
+
const maxEnd = Math.max(...entries.value.map((item) => item.startOffset + (item.ms ?? 0)), 1)
|
|
102
|
+
return entry.ms != null ? Math.round((entry.ms / maxEnd) * 100) : 2
|
|
195
103
|
}
|
|
196
104
|
|
|
197
|
-
function
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
e.ms = undefined
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
e.status = 'ok'
|
|
204
|
-
e.ms = Math.floor(Math.random() * 150 + 20)
|
|
205
|
-
}, 700)
|
|
206
|
-
}
|
|
105
|
+
function formatSize(bytes: number) {
|
|
106
|
+
if (bytes < 1024) {
|
|
107
|
+
return `${bytes}B`
|
|
108
|
+
}
|
|
207
109
|
|
|
208
|
-
|
|
209
|
-
entries.value = []
|
|
210
|
-
selected.value = null
|
|
110
|
+
return `${(bytes / 1024).toFixed(1)}KB`
|
|
211
111
|
}
|
|
212
112
|
</script>
|
|
213
113
|
|
|
214
114
|
<template>
|
|
215
115
|
<div class="view">
|
|
216
|
-
<!-- Stats row -->
|
|
217
116
|
<div class="stats-row">
|
|
218
117
|
<div class="stat-card">
|
|
219
118
|
<div class="stat-label">total</div>
|
|
@@ -233,19 +132,15 @@ function clearAll() {
|
|
|
233
132
|
</div>
|
|
234
133
|
</div>
|
|
235
134
|
|
|
236
|
-
<!-- Toolbar -->
|
|
237
135
|
<div class="toolbar">
|
|
238
136
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
|
|
239
137
|
<button :class="{ 'danger-active': filter === 'error' }" @click="filter = 'error'">errors</button>
|
|
240
138
|
<button :class="{ active: filter === 'pending' }" @click="filter = 'pending'">pending</button>
|
|
241
139
|
<button :class="{ active: filter === 'cached' }" @click="filter = 'cached'">cached</button>
|
|
242
140
|
<input v-model="search" type="search" placeholder="search key or url…" style="max-width: 240px; margin-left: auto" />
|
|
243
|
-
<button @click="clearAll">clear</button>
|
|
244
141
|
</div>
|
|
245
142
|
|
|
246
|
-
<!-- Split view -->
|
|
247
143
|
<div class="split">
|
|
248
|
-
<!-- Table -->
|
|
249
144
|
<div class="table-wrap">
|
|
250
145
|
<table class="data-table">
|
|
251
146
|
<thead>
|
|
@@ -260,9 +155,14 @@ function clearAll() {
|
|
|
260
155
|
</tr>
|
|
261
156
|
</thead>
|
|
262
157
|
<tbody>
|
|
263
|
-
<tr
|
|
158
|
+
<tr
|
|
159
|
+
v-for="entry in filtered"
|
|
160
|
+
:key="entry.id"
|
|
161
|
+
:class="{ selected: selected?.id === entry.id }"
|
|
162
|
+
@click="selectedId = entry.id"
|
|
163
|
+
>
|
|
264
164
|
<td>
|
|
265
|
-
<span class="mono" style="font-size: 11px; color: var(--text2)">{{
|
|
165
|
+
<span class="mono" style="font-size: 11px; color: var(--text2)">{{ entry.key }}</span>
|
|
266
166
|
</td>
|
|
267
167
|
<td>
|
|
268
168
|
<span
|
|
@@ -275,25 +175,25 @@ function clearAll() {
|
|
|
275
175
|
text-overflow: ellipsis;
|
|
276
176
|
white-space: nowrap;
|
|
277
177
|
"
|
|
278
|
-
:title="
|
|
178
|
+
:title="entry.url"
|
|
279
179
|
>
|
|
280
|
-
{{
|
|
180
|
+
{{ entry.url }}
|
|
281
181
|
</span>
|
|
282
182
|
</td>
|
|
283
183
|
<td>
|
|
284
|
-
<span class="badge" :class="statusClass(
|
|
184
|
+
<span class="badge" :class="statusClass(entry.status)">{{ entry.status }}</span>
|
|
285
185
|
</td>
|
|
286
186
|
<td>
|
|
287
|
-
<span class="badge" :class="
|
|
187
|
+
<span class="badge" :class="entry.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ entry.origin }}</span>
|
|
288
188
|
</td>
|
|
289
|
-
<td class="muted text-sm">{{
|
|
290
|
-
<td class="mono text-sm">{{
|
|
189
|
+
<td class="muted text-sm">{{ entry.size ? formatSize(entry.size) : '—' }}</td>
|
|
190
|
+
<td class="mono text-sm">{{ entry.ms != null ? `${entry.ms}ms` : '—' }}</td>
|
|
291
191
|
<td>
|
|
292
192
|
<div style="height: 4px; background: var(--bg2); border-radius: 2px; overflow: hidden">
|
|
293
193
|
<div
|
|
294
194
|
:style="{
|
|
295
|
-
width: barWidth(
|
|
296
|
-
background: barColor(
|
|
195
|
+
width: `${barWidth(entry)}%`,
|
|
196
|
+
background: barColor(entry.status),
|
|
297
197
|
height: '100%',
|
|
298
198
|
borderRadius: '2px',
|
|
299
199
|
}"
|
|
@@ -301,24 +201,27 @@ function clearAll() {
|
|
|
301
201
|
</div>
|
|
302
202
|
</td>
|
|
303
203
|
</tr>
|
|
204
|
+
<tr v-if="!filtered.length">
|
|
205
|
+
<td colspan="7" style="text-align: center; color: var(--text3); padding: 24px">
|
|
206
|
+
{{ connected ? 'No fetches recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
207
|
+
</td>
|
|
208
|
+
</tr>
|
|
304
209
|
</tbody>
|
|
305
210
|
</table>
|
|
306
211
|
</div>
|
|
307
212
|
|
|
308
|
-
<!-- Detail panel -->
|
|
309
213
|
<div v-if="selected" class="detail-panel">
|
|
310
214
|
<div class="detail-header">
|
|
311
215
|
<span class="mono bold" style="font-size: 12px">{{ selected.key }}</span>
|
|
312
216
|
<div class="flex gap-2">
|
|
313
|
-
<button @click="
|
|
314
|
-
<button @click="selected = null">×</button>
|
|
217
|
+
<button @click="selectedId = null">×</button>
|
|
315
218
|
</div>
|
|
316
219
|
</div>
|
|
317
220
|
|
|
318
221
|
<div class="meta-grid">
|
|
319
|
-
<template v-for="[
|
|
320
|
-
<span class="muted text-sm">{{
|
|
321
|
-
<span class="mono text-sm" style="word-break: break-all">{{
|
|
222
|
+
<template v-for="[key, value] in metaRows" :key="key">
|
|
223
|
+
<span class="muted text-sm">{{ key }}</span>
|
|
224
|
+
<span class="mono text-sm" style="word-break: break-all">{{ value }}</span>
|
|
322
225
|
</template>
|
|
323
226
|
</div>
|
|
324
227
|
|
|
@@ -331,28 +234,27 @@ function clearAll() {
|
|
|
331
234
|
<div v-else class="detail-empty">select a call to inspect</div>
|
|
332
235
|
</div>
|
|
333
236
|
|
|
334
|
-
<!-- Waterfall -->
|
|
335
237
|
<div class="waterfall">
|
|
336
238
|
<div class="section-label" style="margin-bottom: 6px">waterfall</div>
|
|
337
|
-
<div v-for="
|
|
239
|
+
<div v-for="entry in entries" :key="entry.id" class="wf-row">
|
|
338
240
|
<span
|
|
339
241
|
class="mono muted text-sm"
|
|
340
242
|
style="width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0"
|
|
341
243
|
>
|
|
342
|
-
{{
|
|
244
|
+
{{ entry.key }}
|
|
343
245
|
</span>
|
|
344
246
|
<div class="wf-track">
|
|
345
247
|
<div
|
|
346
248
|
class="wf-bar"
|
|
347
249
|
:style="{
|
|
348
|
-
left: wfLeft(
|
|
349
|
-
width: Math.max(2, wfWidth(
|
|
350
|
-
background: barColor(
|
|
250
|
+
left: `${wfLeft(entry)}%`,
|
|
251
|
+
width: `${Math.max(2, wfWidth(entry))}%`,
|
|
252
|
+
background: barColor(entry.status),
|
|
351
253
|
}"
|
|
352
254
|
></div>
|
|
353
255
|
</div>
|
|
354
256
|
<span class="mono muted text-sm" style="width: 44px; text-align: right; flex-shrink: 0">
|
|
355
|
-
{{
|
|
257
|
+
{{ entry.ms != null ? `${entry.ms}ms` : '—' }}
|
|
356
258
|
</span>
|
|
357
259
|
</div>
|
|
358
260
|
</div>
|