nuxt-devtools-observatory 0.1.6 → 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-CjQU78-e.css → index-mu192QeW.css} +1 -1
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +144 -122
- package/client/src/views/ComposableTracker.vue +99 -58
- package/client/src/views/FetchDashboard.vue +102 -90
- package/client/src/views/ProvideInjectGraph.vue +149 -76
- package/client/src/views/RenderHeatmap.vue +194 -46
- package/dist/module.json +1 -1
- package/dist/module.mjs +42 -21
- package/dist/runtime/composables/fetch-registry.js +4 -1
- package/dist/runtime/composables/render-registry.js +50 -9
- package/package.json +5 -3
- package/client/dist/assets/index-lG5ffIvt.js +0 -17
|
@@ -1,109 +1,118 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
-
import { useObservatoryData } from '../stores/observatory'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
id: string
|
|
7
|
-
key: string
|
|
8
|
-
url: string
|
|
9
|
-
status: 'pending' | 'ok' | 'error' | 'cached'
|
|
10
|
-
origin: 'ssr' | 'csr'
|
|
11
|
-
ms?: number
|
|
12
|
-
size?: number
|
|
13
|
-
cached: boolean
|
|
14
|
-
payload?: unknown
|
|
15
|
-
file?: string
|
|
16
|
-
line?: number
|
|
17
|
-
startOffset?: number
|
|
18
|
-
}
|
|
3
|
+
import { useObservatoryData, type FetchEntry } from '../stores/observatory'
|
|
4
|
+
|
|
5
|
+
type FetchViewEntry = FetchEntry & { startOffset: number }
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
const { fetches: entries } = useObservatoryData()
|
|
7
|
+
const { fetch, connected } = useObservatoryData()
|
|
22
8
|
|
|
23
9
|
const filter = ref<string>('all')
|
|
24
10
|
const search = ref('')
|
|
25
|
-
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)
|
|
26
24
|
|
|
27
25
|
const counts = computed(() => ({
|
|
28
|
-
ok: entries.value
|
|
29
|
-
pending: entries.value
|
|
30
|
-
error: entries.value
|
|
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,
|
|
31
29
|
}))
|
|
32
30
|
|
|
33
31
|
const filtered = computed(() => {
|
|
34
|
-
return
|
|
35
|
-
if (filter.value !== 'all' &&
|
|
32
|
+
return entries.value.filter((entry) => {
|
|
33
|
+
if (filter.value !== 'all' && entry.status !== filter.value) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
36
37
|
const q = search.value.toLowerCase()
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
if (q && !entry.key.toLowerCase().includes(q) && !entry.url.toLowerCase().includes(q)) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
return true
|
|
39
44
|
})
|
|
40
45
|
})
|
|
41
46
|
|
|
42
47
|
const metaRows = computed(() => {
|
|
43
|
-
if (!selected.value)
|
|
44
|
-
|
|
48
|
+
if (!selected.value) {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = selected.value
|
|
53
|
+
|
|
45
54
|
return [
|
|
46
|
-
['url',
|
|
47
|
-
['status',
|
|
48
|
-
['origin',
|
|
49
|
-
['duration',
|
|
50
|
-
['size',
|
|
51
|
-
['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'],
|
|
52
61
|
]
|
|
53
62
|
})
|
|
54
63
|
|
|
55
64
|
const payloadStr = computed(() => {
|
|
56
|
-
if (!selected.value)
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
|
|
59
75
|
try {
|
|
60
|
-
return JSON.stringify(
|
|
76
|
+
return JSON.stringify(payload, null, 2)
|
|
61
77
|
} catch {
|
|
62
|
-
return String(
|
|
78
|
+
return String(payload)
|
|
63
79
|
}
|
|
64
80
|
})
|
|
65
81
|
|
|
66
|
-
function statusClass(
|
|
67
|
-
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'
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
function barColor(
|
|
71
|
-
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)'
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
function barWidth(
|
|
75
|
-
const maxMs = Math.max(...
|
|
76
|
-
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
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
function wfLeft(
|
|
80
|
-
const maxEnd = Math.max(...
|
|
81
|
-
return Math.round((
|
|
82
|
-
}
|
|
83
|
-
function wfWidth(e: FetchEntry) {
|
|
84
|
-
const maxEnd = Math.max(...(entries.value ?? []).map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
85
|
-
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)
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
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
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
105
|
+
function formatSize(bytes: number) {
|
|
106
|
+
if (bytes < 1024) {
|
|
107
|
+
return `${bytes}B`
|
|
108
|
+
}
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
// No-op: cannot clear live registry from client
|
|
100
|
-
selected.value = null
|
|
110
|
+
return `${(bytes / 1024).toFixed(1)}KB`
|
|
101
111
|
}
|
|
102
112
|
</script>
|
|
103
113
|
|
|
104
114
|
<template>
|
|
105
115
|
<div class="view">
|
|
106
|
-
<!-- Stats row -->
|
|
107
116
|
<div class="stats-row">
|
|
108
117
|
<div class="stat-card">
|
|
109
118
|
<div class="stat-label">total</div>
|
|
@@ -123,19 +132,15 @@ function clearAll() {
|
|
|
123
132
|
</div>
|
|
124
133
|
</div>
|
|
125
134
|
|
|
126
|
-
<!-- Toolbar -->
|
|
127
135
|
<div class="toolbar">
|
|
128
136
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
|
|
129
137
|
<button :class="{ 'danger-active': filter === 'error' }" @click="filter = 'error'">errors</button>
|
|
130
138
|
<button :class="{ active: filter === 'pending' }" @click="filter = 'pending'">pending</button>
|
|
131
139
|
<button :class="{ active: filter === 'cached' }" @click="filter = 'cached'">cached</button>
|
|
132
140
|
<input v-model="search" type="search" placeholder="search key or url…" style="max-width: 240px; margin-left: auto" />
|
|
133
|
-
<button @click="clearAll">clear</button>
|
|
134
141
|
</div>
|
|
135
142
|
|
|
136
|
-
<!-- Split view -->
|
|
137
143
|
<div class="split">
|
|
138
|
-
<!-- Table -->
|
|
139
144
|
<div class="table-wrap">
|
|
140
145
|
<table class="data-table">
|
|
141
146
|
<thead>
|
|
@@ -150,9 +155,14 @@ function clearAll() {
|
|
|
150
155
|
</tr>
|
|
151
156
|
</thead>
|
|
152
157
|
<tbody>
|
|
153
|
-
<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
|
+
>
|
|
154
164
|
<td>
|
|
155
|
-
<span class="mono" style="font-size: 11px; color: var(--text2)">{{
|
|
165
|
+
<span class="mono" style="font-size: 11px; color: var(--text2)">{{ entry.key }}</span>
|
|
156
166
|
</td>
|
|
157
167
|
<td>
|
|
158
168
|
<span
|
|
@@ -165,25 +175,25 @@ function clearAll() {
|
|
|
165
175
|
text-overflow: ellipsis;
|
|
166
176
|
white-space: nowrap;
|
|
167
177
|
"
|
|
168
|
-
:title="
|
|
178
|
+
:title="entry.url"
|
|
169
179
|
>
|
|
170
|
-
{{
|
|
180
|
+
{{ entry.url }}
|
|
171
181
|
</span>
|
|
172
182
|
</td>
|
|
173
183
|
<td>
|
|
174
|
-
<span class="badge" :class="statusClass(
|
|
184
|
+
<span class="badge" :class="statusClass(entry.status)">{{ entry.status }}</span>
|
|
175
185
|
</td>
|
|
176
186
|
<td>
|
|
177
|
-
<span class="badge" :class="
|
|
187
|
+
<span class="badge" :class="entry.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ entry.origin }}</span>
|
|
178
188
|
</td>
|
|
179
|
-
<td class="muted text-sm">{{
|
|
180
|
-
<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>
|
|
181
191
|
<td>
|
|
182
192
|
<div style="height: 4px; background: var(--bg2); border-radius: 2px; overflow: hidden">
|
|
183
193
|
<div
|
|
184
194
|
:style="{
|
|
185
|
-
width: barWidth(
|
|
186
|
-
background: barColor(
|
|
195
|
+
width: `${barWidth(entry)}%`,
|
|
196
|
+
background: barColor(entry.status),
|
|
187
197
|
height: '100%',
|
|
188
198
|
borderRadius: '2px',
|
|
189
199
|
}"
|
|
@@ -191,24 +201,27 @@ function clearAll() {
|
|
|
191
201
|
</div>
|
|
192
202
|
</td>
|
|
193
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>
|
|
194
209
|
</tbody>
|
|
195
210
|
</table>
|
|
196
211
|
</div>
|
|
197
212
|
|
|
198
|
-
<!-- Detail panel -->
|
|
199
213
|
<div v-if="selected" class="detail-panel">
|
|
200
214
|
<div class="detail-header">
|
|
201
215
|
<span class="mono bold" style="font-size: 12px">{{ selected.key }}</span>
|
|
202
216
|
<div class="flex gap-2">
|
|
203
|
-
<button @click="
|
|
204
|
-
<button @click="selected = null">×</button>
|
|
217
|
+
<button @click="selectedId = null">×</button>
|
|
205
218
|
</div>
|
|
206
219
|
</div>
|
|
207
220
|
|
|
208
221
|
<div class="meta-grid">
|
|
209
|
-
<template v-for="[
|
|
210
|
-
<span class="muted text-sm">{{
|
|
211
|
-
<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>
|
|
212
225
|
</template>
|
|
213
226
|
</div>
|
|
214
227
|
|
|
@@ -221,28 +234,27 @@ function clearAll() {
|
|
|
221
234
|
<div v-else class="detail-empty">select a call to inspect</div>
|
|
222
235
|
</div>
|
|
223
236
|
|
|
224
|
-
<!-- Waterfall -->
|
|
225
237
|
<div class="waterfall">
|
|
226
238
|
<div class="section-label" style="margin-bottom: 6px">waterfall</div>
|
|
227
|
-
<div v-for="
|
|
239
|
+
<div v-for="entry in entries" :key="entry.id" class="wf-row">
|
|
228
240
|
<span
|
|
229
241
|
class="mono muted text-sm"
|
|
230
242
|
style="width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0"
|
|
231
243
|
>
|
|
232
|
-
{{
|
|
244
|
+
{{ entry.key }}
|
|
233
245
|
</span>
|
|
234
246
|
<div class="wf-track">
|
|
235
247
|
<div
|
|
236
248
|
class="wf-bar"
|
|
237
249
|
:style="{
|
|
238
|
-
left: wfLeft(
|
|
239
|
-
width: Math.max(2, wfWidth(
|
|
240
|
-
background: barColor(
|
|
250
|
+
left: `${wfLeft(entry)}%`,
|
|
251
|
+
width: `${Math.max(2, wfWidth(entry))}%`,
|
|
252
|
+
background: barColor(entry.status),
|
|
241
253
|
}"
|
|
242
254
|
></div>
|
|
243
255
|
</div>
|
|
244
256
|
<span class="mono muted text-sm" style="width: 44px; text-align: right; flex-shrink: 0">
|
|
245
|
-
{{
|
|
257
|
+
{{ entry.ms != null ? `${entry.ms}ms` : '—' }}
|
|
246
258
|
</span>
|
|
247
259
|
</div>
|
|
248
260
|
</div>
|