nuxt-devtools-observatory 0.1.32 → 0.1.34

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 (36) hide show
  1. package/README.md +37 -1
  2. package/client/.env.example +2 -0
  3. package/client/dist/assets/index-BO7neKEi.css +1 -0
  4. package/client/dist/assets/index-fFBuk6M6.js +20 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +8 -0
  7. package/client/src/components/Flamegraph.vue +4 -4
  8. package/client/src/components/SpanInspector.vue +1 -1
  9. package/client/src/composables/composable-search.ts +3 -0
  10. package/client/src/composables/trace-render-aggregation.ts +11 -2
  11. package/client/src/composables/useVirtualizationConfig.ts +40 -0
  12. package/client/src/composables/useVirtualizationFlags.ts +129 -0
  13. package/client/src/stores/observatory.ts +20 -0
  14. package/client/src/views/ComposableTracker.vue +212 -71
  15. package/client/src/views/FetchDashboard.vue +181 -16
  16. package/client/src/views/PiniaStoreTracker.vue +343 -0
  17. package/client/src/views/ProvideInjectGraph.vue +66 -18
  18. package/client/src/views/RenderHeatmap.vue +329 -75
  19. package/client/src/views/TraceViewer.vue +190 -20
  20. package/client/src/views/TransitionTimeline.vue +112 -19
  21. package/dist/module.d.mts +15 -0
  22. package/dist/module.json +1 -1
  23. package/dist/module.mjs +28 -24
  24. package/dist/runtime/composables/pinia-store-registry.d.ts +44 -0
  25. package/dist/runtime/composables/pinia-store-registry.js +447 -0
  26. package/dist/runtime/composables/provide-inject-registry.js +13 -8
  27. package/dist/runtime/composables/render-registry.js +6 -4
  28. package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
  29. package/dist/runtime/instrumentation/fetch.d.ts +7 -1
  30. package/dist/runtime/instrumentation/fetch.js +22 -1
  31. package/dist/runtime/plugin.js +39 -2
  32. package/dist/runtime/test-bridge.d.ts +18 -0
  33. package/dist/runtime/test-bridge.js +100 -0
  34. package/package.json +14 -3
  35. package/client/dist/assets/index-5Wl1XYRH.js +0 -17
  36. package/client/dist/assets/index-DT_QUiIh.css +0 -1
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
+ import { useVirtualizer } from '@tanstack/vue-virtual'
4
+ import { useVirtualizationConfig } from '@observatory-client/composables/useVirtualizationConfig'
5
+ import { useVirtualizationFlags } from '@observatory-client/composables/useVirtualizationFlags'
3
6
  import {
4
7
  useObservatoryData,
5
8
  setComposableMode,
@@ -93,6 +96,10 @@ function openInEditor(file: string) {
93
96
  const filter = ref('all')
94
97
  const search = ref('')
95
98
  const expanded = ref<string | null>(null)
99
+ const listScrollRef = ref<HTMLElement | null>(null)
100
+
101
+ const { effective: virtualizationFlags } = useVirtualizationFlags()
102
+ const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 88, overscan: 6 })
96
103
 
97
104
  const entries = computed<RuntimeComposableEntry[]>(() => rawEntries.value)
98
105
 
@@ -142,6 +149,99 @@ const filtered = computed(() => {
142
149
  return [...layoutEntries, ...regularEntries]
143
150
  })
144
151
 
152
+ const virtualizedCardsEnabled = computed(() => virtualizationFlags.value.composables && expanded.value === null)
153
+
154
+ const listVirtualizerOptions = computed(() => ({
155
+ count: filtered.value.length,
156
+ getScrollElement: () => listScrollRef.value,
157
+ estimateSize: () => virtualizationPreset.value.rowHeight,
158
+ overscan: virtualizationPreset.value.overscan,
159
+ }))
160
+
161
+ const listVirtualizer = useVirtualizer(listVirtualizerOptions)
162
+
163
+ const listVirtualItems = computed(() => {
164
+ if (!virtualizedCardsEnabled.value) {
165
+ return []
166
+ }
167
+
168
+ return listVirtualizer.value.getVirtualItems()
169
+ })
170
+
171
+ const topListPadding = computed(() => {
172
+ if (!virtualizedCardsEnabled.value || listVirtualItems.value.length === 0) {
173
+ return 0
174
+ }
175
+
176
+ return listVirtualItems.value[0].start
177
+ })
178
+
179
+ const bottomListPadding = computed(() => {
180
+ if (!virtualizedCardsEnabled.value || listVirtualItems.value.length === 0) {
181
+ return 0
182
+ }
183
+
184
+ const total = listVirtualizer.value.getTotalSize()
185
+ const last = listVirtualItems.value[listVirtualItems.value.length - 1]
186
+
187
+ return Math.max(0, total - last.end)
188
+ })
189
+
190
+ const visibleEntries = computed(() => {
191
+ if (!virtualizedCardsEnabled.value) {
192
+ return filtered.value
193
+ }
194
+
195
+ return listVirtualItems.value
196
+ .map((item) => filtered.value[item.index])
197
+ .filter((entry): entry is RuntimeComposableEntry => Boolean(entry))
198
+ })
199
+
200
+ const visibleEntryCards = computed<VisibleEntryCard[]>(() => {
201
+ return visibleEntries.value.map((entry) => {
202
+ const refEntries = Object.entries(entry.refs) as Array<[string, RuntimeComposableEntry['refs'][string]]>
203
+ const detailRefs: RefRowView[] = refEntries.map(([key, ref]) => {
204
+ const isLong = isLongValue(ref.value)
205
+ const expanded = isLong && isRefExpanded(entry.id, key)
206
+ const shared = Boolean(entry.sharedKeys?.includes(key))
207
+
208
+ return {
209
+ key,
210
+ ref,
211
+ isLong,
212
+ expanded,
213
+ displayValue: isLong && !expanded ? formatVal(ref.value) : formatValFull(ref.value),
214
+ typeClass: typeBadgeClass(ref.type),
215
+ shared,
216
+ }
217
+ })
218
+
219
+ const history = entry.history ?? []
220
+ const historyRows: HistoryRowView[] = []
221
+ const end = Math.max(history.length - 20, 0)
222
+
223
+ for (let i = history.length - 1; i >= end; i--) {
224
+ const evt = history[i]
225
+ historyRows.push({
226
+ id: `${entry.id}-${evt.key}-${evt.t}-${i}`,
227
+ timeLabel: `+${(evt.t / 1000).toFixed(2)}s`,
228
+ key: evt.key,
229
+ value: formatVal(evt.value),
230
+ })
231
+ }
232
+
233
+ return {
234
+ entry,
235
+ refCount: refEntries.length,
236
+ previewRefs: detailRefs.slice(0, 3),
237
+ detailRefs,
238
+ historyRows,
239
+ historyHiddenCount: Math.max(0, history.length - 20),
240
+ lifecycle: lifecycleRows(entry),
241
+ }
242
+ })
243
+ })
244
+
145
245
  function lifecycleRows(entry: RuntimeComposableEntry) {
146
246
  return [
147
247
  {
@@ -229,6 +329,33 @@ interface LookupTarget {
229
329
  identityGroup?: string
230
330
  }
231
331
 
332
+ interface RefRowView {
333
+ key: string
334
+ ref: RuntimeComposableEntry['refs'][string]
335
+ isLong: boolean
336
+ expanded: boolean
337
+ displayValue: string
338
+ typeClass: string
339
+ shared: boolean
340
+ }
341
+
342
+ interface HistoryRowView {
343
+ id: string
344
+ timeLabel: string
345
+ key: string
346
+ value: string
347
+ }
348
+
349
+ interface VisibleEntryCard {
350
+ entry: RuntimeComposableEntry
351
+ refCount: number
352
+ previewRefs: RefRowView[]
353
+ detailRefs: RefRowView[]
354
+ historyRows: HistoryRowView[]
355
+ historyHiddenCount: number
356
+ lifecycle: ReturnType<typeof lifecycleRows>
357
+ }
358
+
232
359
  const lookupTarget = ref<LookupTarget | null>(null)
233
360
 
234
361
  const lookupResults = computed(() => {
@@ -371,131 +498,133 @@ function applyEdit() {
371
498
  </button>
372
499
  </div>
373
500
 
374
- <div class="composable-tracker__list">
501
+ <div ref="listScrollRef" class="composable-tracker__list">
375
502
  <div
376
- v-for="entry in filtered"
377
- :key="entry.id"
503
+ v-if="virtualizedCardsEnabled && topListPadding > 0"
504
+ class="composable-tracker__virtual-spacer"
505
+ :style="{ height: `${topListPadding}px` }"
506
+ aria-hidden="true"
507
+ />
508
+ <div
509
+ v-for="card in visibleEntryCards"
510
+ :key="card.entry.id"
378
511
  class="composable-tracker__card"
379
512
  :class="{
380
- 'composable-tracker__card--leak': entry.leak,
381
- 'composable-tracker__card--expanded': expanded === entry.id,
513
+ 'composable-tracker__card--leak': card.entry.leak,
514
+ 'composable-tracker__card--expanded': expanded === card.entry.id,
382
515
  }"
383
- @click="expanded = expanded === entry.id ? null : entry.id"
516
+ @click="expanded = expanded === card.entry.id ? null : card.entry.id"
384
517
  >
385
518
  <div class="composable-tracker__card-header">
386
519
  <div class="composable-tracker__identity">
387
- <span class="composable-tracker__name mono">{{ entry.name }}</span>
388
- <span class="composable-tracker__file muted mono">{{ basename(entry.componentFile) }}</span>
520
+ <span class="composable-tracker__name mono">{{ card.entry.name }}</span>
521
+ <span class="composable-tracker__file muted mono">{{ basename(card.entry.componentFile) }}</span>
389
522
  </div>
390
523
  <div class="composable-tracker__meta">
391
- <span v-if="entry.watcherCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.watcherCount }}w</span>
392
- <span v-if="entry.intervalCount > 0 && !entry.leak" class="badge badge-warn">{{ entry.intervalCount }}t</span>
393
- <span v-if="entry.leak" class="badge badge-err">leak</span>
394
- <span v-else-if="entry.status === 'mounted'" class="badge badge-ok">mounted</span>
524
+ <span v-if="card.entry.watcherCount > 0 && !card.entry.leak" class="badge badge-warn">
525
+ {{ card.entry.watcherCount }}w
526
+ </span>
527
+ <span v-if="card.entry.intervalCount > 0 && !card.entry.leak" class="badge badge-warn">
528
+ {{ card.entry.intervalCount }}t
529
+ </span>
530
+ <span v-if="card.entry.leak" class="badge badge-err">leak</span>
531
+ <span v-else-if="card.entry.status === 'mounted'" class="badge badge-ok">mounted</span>
395
532
  <span v-else class="badge badge-gray">unmounted</span>
396
533
  </div>
397
534
  </div>
398
535
 
399
536
  <!-- Inline ref preview — shows up to 3 refs without expanding -->
400
- <div v-if="Object.keys(entry.refs).length" class="composable-tracker__refs-preview">
537
+ <div v-if="card.refCount" class="composable-tracker__refs-preview">
401
538
  <span
402
- v-for="[k, v] in Object.entries(entry.refs).slice(0, 3)"
403
- :key="k"
539
+ v-for="row in card.previewRefs"
540
+ :key="row.key"
404
541
  class="composable-tracker__ref-chip"
405
542
  :class="{
406
- 'composable-tracker__ref-chip--reactive': v.type === 'reactive',
407
- 'composable-tracker__ref-chip--computed': v.type === 'computed',
408
- 'composable-tracker__ref-chip--shared': entry.sharedKeys?.includes(k),
543
+ 'composable-tracker__ref-chip--reactive': row.ref.type === 'reactive',
544
+ 'composable-tracker__ref-chip--computed': row.ref.type === 'computed',
545
+ 'composable-tracker__ref-chip--shared': row.shared,
409
546
  }"
410
- :title="entry.sharedKeys?.includes(k) ? 'shared global state' : ''"
547
+ :title="row.shared ? 'shared global state' : ''"
411
548
  >
412
- <span class="composable-tracker__ref-chip-key">{{ k }}</span>
413
- <span class="composable-tracker__ref-chip-val">{{ formatVal(v.value) }}</span>
414
- <span v-if="entry.sharedKeys?.includes(k)" class="composable-tracker__ref-chip-shared-dot" title="global"></span>
415
- </span>
416
- <span v-if="Object.keys(entry.refs).length > 3" class="muted text-sm">
417
- +{{ Object.keys(entry.refs).length - 3 }} more
549
+ <span class="composable-tracker__ref-chip-key">{{ row.key }}</span>
550
+ <span class="composable-tracker__ref-chip-val">{{ formatVal(row.ref.value) }}</span>
551
+ <span v-if="row.shared" class="composable-tracker__ref-chip-shared-dot" title="global"></span>
418
552
  </span>
553
+ <span v-if="card.refCount > 3" class="muted text-sm">+{{ card.refCount - 3 }} more</span>
419
554
  </div>
420
555
 
421
- <div v-if="expanded === entry.id" class="composable-tracker__detail" @click.stop>
422
- <div v-if="entry.leak" class="composable-tracker__leak-banner">{{ entry.leakReason }}</div>
556
+ <div v-if="expanded === card.entry.id" class="composable-tracker__detail" @click.stop>
557
+ <div v-if="card.entry.leak" class="composable-tracker__leak-banner">{{ card.entry.leakReason }}</div>
423
558
 
424
559
  <!-- Global state warning -->
425
- <div v-if="entry.sharedKeys?.length" class="composable-tracker__global-banner">
560
+ <div v-if="card.entry.sharedKeys?.length" class="composable-tracker__global-banner">
426
561
  <span class="composable-tracker__global-dot"></span>
427
562
  <span>
428
563
  <strong>global state</strong>
429
- — {{ entry.sharedKeys.join(', ') }}
430
- {{ entry.sharedKeys.length === 1 ? 'is' : 'are' }}
431
- shared across all instances of {{ entry.name }}
564
+ — {{ card.entry.sharedKeys.join(', ') }}
565
+ {{ card.entry.sharedKeys.length === 1 ? 'is' : 'are' }}
566
+ shared across all instances of {{ card.entry.name }}
432
567
  </span>
433
568
  </div>
434
569
 
435
570
  <div class="composable-tracker__section-label tracker-section-label">reactive state</div>
436
- <div v-if="!Object.keys(entry.refs).length" class="composable-tracker__compact-muted muted text-sm">
437
- no tracked state returned
438
- </div>
439
- <div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="composable-tracker__ref-row">
571
+ <div v-if="!card.refCount" class="composable-tracker__compact-muted muted text-sm">no tracked state returned</div>
572
+ <div v-for="row in card.detailRefs" :key="row.key" class="composable-tracker__ref-row">
440
573
  <span
441
574
  class="composable-tracker__ref-key composable-tracker__ref-key--clickable mono text-sm"
442
575
  :title="
443
- entry.sharedKeyGroups?.[k]
444
- ? `click to see instances sharing this exact '${k}' state`
445
- : `click to see all instances exposing '${k}'`
576
+ card.entry.sharedKeyGroups?.[row.key]
577
+ ? `click to see instances sharing this exact '${row.key}' state`
578
+ : `click to see all instances exposing '${row.key}'`
446
579
  "
447
- @click.stop="openLookup(entry, k)"
580
+ @click.stop="openLookup(card.entry, row.key)"
448
581
  >
449
- {{ k }}
582
+ {{ row.key }}
450
583
  </span>
451
584
  <span
452
585
  class="composable-tracker__ref-val mono text-sm"
453
586
  :class="{
454
- 'composable-tracker__ref-val--full': isLongValue(v.value) && isRefExpanded(entry.id, k),
455
- 'composable-tracker__ref-val--collapsed': isLongValue(v.value) && !isRefExpanded(entry.id, k),
587
+ 'composable-tracker__ref-val--full': row.isLong && row.expanded,
588
+ 'composable-tracker__ref-val--collapsed': row.isLong && !row.expanded,
456
589
  }"
457
590
  >
458
- {{ isLongValue(v.value) && !isRefExpanded(entry.id, k) ? formatVal(v.value) : formatValFull(v.value) }}
591
+ {{ row.displayValue }}
459
592
  </span>
460
593
  <div class="composable-tracker__ref-row-actions">
461
594
  <button
462
- v-if="isLongValue(v.value)"
595
+ v-if="row.isLong"
463
596
  class="composable-tracker__expand-btn"
464
- :title="isRefExpanded(entry.id, k) ? 'Collapse' : 'Expand'"
465
- @click.stop="toggleRefExpand(entry.id, k)"
597
+ :title="row.expanded ? 'Collapse' : 'Expand'"
598
+ @click.stop="toggleRefExpand(card.entry.id, row.key)"
466
599
  >
467
- {{ isRefExpanded(entry.id, k) ? '▲' : '▼' }}
600
+ {{ row.expanded ? '▲' : '▼' }}
468
601
  </button>
469
- <span class="badge text-xs" :class="typeBadgeClass(v.type)">{{ v.type }}</span>
470
- <span v-if="entry.sharedKeys?.includes(k)" class="badge badge-amber text-xs">global</span>
602
+ <span class="badge text-xs" :class="row.typeClass">{{ row.ref.type }}</span>
603
+ <span v-if="row.shared" class="badge badge-amber text-xs">global</span>
471
604
  <button
472
- v-if="v.type === 'ref'"
605
+ v-if="row.ref.type === 'ref'"
473
606
  class="composable-tracker__edit-btn"
474
607
  title="Edit value"
475
- @click.stop="openEdit(entry.id, k, v.value)"
608
+ @click.stop="openEdit(card.entry.id, row.key, row.ref.value)"
476
609
  >
477
610
  edit
478
611
  </button>
479
612
  </div>
480
613
  </div>
481
614
 
482
- <template v-if="entry.history && entry.history.length">
615
+ <template v-if="card.historyRows.length">
483
616
  <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
484
617
  change history
485
- <span class="composable-tracker__section-label-meta muted">({{ entry.history.length }} events)</span>
618
+ <span class="composable-tracker__section-label-meta muted">({{ card.entry.history.length }} events)</span>
486
619
  </div>
487
620
  <div class="composable-tracker__history-list">
488
- <div
489
- v-for="(evt, idx) in [...entry.history].reverse().slice(0, 20)"
490
- :key="idx"
491
- class="composable-tracker__history-row"
492
- >
493
- <span class="composable-tracker__history-time mono muted">+{{ (evt.t / 1000).toFixed(2) }}s</span>
494
- <span class="composable-tracker__history-key mono">{{ evt.key }}</span>
495
- <span class="composable-tracker__history-val mono">{{ formatVal(evt.value) }}</span>
621
+ <div v-for="historyRow in card.historyRows" :key="historyRow.id" class="composable-tracker__history-row">
622
+ <span class="composable-tracker__history-time mono muted">{{ historyRow.timeLabel }}</span>
623
+ <span class="composable-tracker__history-key mono">{{ historyRow.key }}</span>
624
+ <span class="composable-tracker__history-val mono">{{ historyRow.value }}</span>
496
625
  </div>
497
- <div v-if="entry.history.length > 20" class="composable-tracker__compact-muted muted text-sm">
498
- … {{ entry.history.length - 20 }} earlier events
626
+ <div v-if="card.historyHiddenCount > 0" class="composable-tracker__compact-muted muted text-sm">
627
+ … {{ card.historyHiddenCount }} earlier events
499
628
  </div>
500
629
  </div>
501
630
  </template>
@@ -503,7 +632,7 @@ function applyEdit() {
503
632
  <div class="composable-tracker__section-label composable-tracker__section-label--spaced tracker-section-label">
504
633
  lifecycle
505
634
  </div>
506
- <div v-for="row in lifecycleRows(entry)" :key="row.label" class="composable-tracker__lifecycle-row">
635
+ <div v-for="row in card.lifecycle" :key="row.label" class="composable-tracker__lifecycle-row">
507
636
  <span
508
637
  class="composable-tracker__lifecycle-dot"
509
638
  :class="row.ok ? 'composable-tracker__lifecycle-dot--ok' : 'composable-tracker__lifecycle-dot--error'"
@@ -522,12 +651,12 @@ function applyEdit() {
522
651
  </div>
523
652
  <div class="composable-tracker__lifecycle-row">
524
653
  <span class="composable-tracker__context-label muted text-sm">component</span>
525
- <span class="composable-tracker__context-value mono text-sm">{{ basename(entry.componentFile) }}</span>
654
+ <span class="composable-tracker__context-value mono text-sm">{{ basename(card.entry.componentFile) }}</span>
526
655
  </div>
527
656
  <div class="composable-tracker__lifecycle-row">
528
657
  <span class="composable-tracker__context-label muted text-sm">uid</span>
529
658
  <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
530
- {{ entry.componentUid }}
659
+ {{ card.entry.componentUid }}
531
660
  </span>
532
661
  </div>
533
662
  <div class="composable-tracker__lifecycle-row">
@@ -535,8 +664,8 @@ function applyEdit() {
535
664
  <span
536
665
  class="composable-tracker__context-value composable-tracker__context-value--row composable-tracker__context-value--muted mono text-sm muted"
537
666
  >
538
- {{ entry.file }}:{{ entry.line }}
539
- <button class="composable-tracker__jump-btn" title="Open in editor" @click.stop="openInEditor(entry.file)">
667
+ {{ card.entry.file }}:{{ card.entry.line }}
668
+ <button class="composable-tracker__jump-btn" title="Open in editor" @click.stop="openInEditor(card.entry.file)">
540
669
  open ↗
541
670
  </button>
542
671
  </span>
@@ -544,20 +673,27 @@ function applyEdit() {
544
673
  <div class="composable-tracker__lifecycle-row">
545
674
  <span class="composable-tracker__context-label muted text-sm">route</span>
546
675
  <span class="composable-tracker__context-value composable-tracker__context-value--muted mono text-sm muted">
547
- {{ entry.route }}
676
+ {{ card.entry.route }}
548
677
  </span>
549
678
  </div>
550
679
  <div class="composable-tracker__lifecycle-row">
551
680
  <span class="composable-tracker__context-label muted text-sm">watchers</span>
552
- <span class="composable-tracker__context-value mono text-sm">{{ entry.watcherCount }}</span>
681
+ <span class="composable-tracker__context-value mono text-sm">{{ card.entry.watcherCount }}</span>
553
682
  </div>
554
683
  <div class="composable-tracker__lifecycle-row">
555
684
  <span class="composable-tracker__context-label muted text-sm">intervals</span>
556
- <span class="composable-tracker__context-value mono text-sm">{{ entry.intervalCount }}</span>
685
+ <span class="composable-tracker__context-value mono text-sm">{{ card.entry.intervalCount }}</span>
557
686
  </div>
558
687
  </div>
559
688
  </div>
560
689
 
690
+ <div
691
+ v-if="virtualizedCardsEnabled && bottomListPadding > 0"
692
+ class="composable-tracker__virtual-spacer"
693
+ :style="{ height: `${bottomListPadding}px` }"
694
+ aria-hidden="true"
695
+ />
696
+
561
697
  <div v-if="!filtered.length" class="composable-tracker__empty muted text-sm">
562
698
  {{ connected ? 'No composables recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
563
699
  </div>
@@ -644,6 +780,11 @@ function applyEdit() {
644
780
  min-height: 0;
645
781
  }
646
782
 
783
+ .composable-tracker__virtual-spacer {
784
+ width: 100%;
785
+ flex-shrink: 0;
786
+ }
787
+
647
788
  .composable-tracker__card {
648
789
  background: var(--bg3);
649
790
  border: var(--tracker-border-width) solid var(--border);