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.
@@ -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
- interface FetchEntry {
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
- // Mock data in production this comes from the WS registry
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 selected = ref<FetchEntry | null>(null)
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((e) => e.status === 'ok').length,
133
- pending: entries.value.filter((e) => e.status === 'pending').length,
134
- error: entries.value.filter((e) => e.status === 'error').length,
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((e) => {
139
- if (filter.value !== 'all' && e.status !== filter.value) return false
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
- if (q && !e.key.includes(q) && !e.url.includes(q)) return false
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) return []
148
- const e = selected.value
48
+ if (!selected.value) {
49
+ return []
50
+ }
51
+
52
+ const entry = selected.value
53
+
149
54
  return [
150
- ['url', e.url],
151
- ['status', e.status],
152
- ['origin', e.origin],
153
- ['duration', e.ms != null ? e.ms + 'ms' : '—'],
154
- ['size', e.size ? formatSize(e.size) : '—'],
155
- ['cached', e.cached ? 'yes' : 'no'],
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) return ''
161
- const p = selected.value.payload
162
- if (!p) return '(no payload captured yet)'
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(p, null, 2)
76
+ return JSON.stringify(payload, null, 2)
165
77
  } catch {
166
- return String(p)
78
+ return String(payload)
167
79
  }
168
80
  })
169
81
 
170
- function statusClass(s: string) {
171
- return { ok: 'badge-ok', error: 'badge-err', pending: 'badge-warn', cached: 'badge-gray' }[s] ?? '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(s: string) {
175
- return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[s] ?? '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(e: FetchEntry) {
179
- const maxMs = Math.max(...entries.value.filter((x) => x.ms).map((x) => x.ms!), 1)
180
- return e.ms != null ? Math.max(4, Math.round((e.ms / maxMs) * 100)) : 4
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(e: FetchEntry) {
184
- const maxEnd = Math.max(...entries.value.map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
185
- return Math.round(((e.startOffset ?? 0) / maxEnd) * 100)
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 formatSize(bytes: number) {
193
- if (bytes < 1024) return bytes + 'B'
194
- return (bytes / 1024).toFixed(1) + 'KB'
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 replayFetch() {
198
- if (!selected.value) return
199
- const e = selected.value
200
- e.status = 'pending'
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
- function clearAll() {
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 v-for="e in filtered" :key="e.id" :class="{ selected: selected?.id === e.id }" @click="selected = e">
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)">{{ e.key }}</span>
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="e.url"
178
+ :title="entry.url"
279
179
  >
280
- {{ e.url }}
180
+ {{ entry.url }}
281
181
  </span>
282
182
  </td>
283
183
  <td>
284
- <span class="badge" :class="statusClass(e.status)">{{ e.status }}</span>
184
+ <span class="badge" :class="statusClass(entry.status)">{{ entry.status }}</span>
285
185
  </td>
286
186
  <td>
287
- <span class="badge" :class="e.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ e.origin }}</span>
187
+ <span class="badge" :class="entry.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ entry.origin }}</span>
288
188
  </td>
289
- <td class="muted text-sm">{{ e.size ? formatSize(e.size) : '—' }}</td>
290
- <td class="mono text-sm">{{ e.ms != null ? e.ms + 'ms' : '—' }}</td>
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(e) + '%',
296
- background: barColor(e.status),
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="replayFetch">↺ replay</button>
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="[k, v] in metaRows" :key="k">
320
- <span class="muted text-sm">{{ k }}</span>
321
- <span class="mono text-sm" style="word-break: break-all">{{ v }}</span>
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="e in entries" :key="e.id" class="wf-row">
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
- {{ e.key }}
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(e) + '%',
349
- width: Math.max(2, wfWidth(e)) + '%',
350
- background: barColor(e.status),
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
- {{ e.ms != null ? e.ms + 'ms' : '—' }}
257
+ {{ entry.ms != null ? `${entry.ms}ms` : '—' }}
356
258
  </span>
357
259
  </div>
358
260
  </div>