nuxt-devtools-observatory 0.1.31 → 0.1.32
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 +74 -46
- package/client/dist/assets/index-5Wl1XYRH.js +17 -0
- package/client/dist/assets/index-DT_QUiIh.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/components/Flamegraph.vue +3 -4
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- package/client/src/composables/composable-search.ts +124 -0
- package/client/src/composables/trace-render-aggregation.ts +254 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +1 -5
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +65 -30
- package/client/src/views/RenderHeatmap.vue +63 -1
- package/client/src/views/TraceViewer.vue +618 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +19 -0
- package/dist/runtime/composables/composable-registry.js +63 -5
- package/dist/runtime/composables/render-registry.js +19 -11
- package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
- package/dist/runtime/nitro/fetch-capture.js +85 -7
- package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
- package/dist/runtime/nitro/ssr-trace-store.js +84 -0
- package/dist/runtime/plugin.js +44 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +5 -1
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.css +0 -1
|
@@ -1,7 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import type {
|
|
5
|
+
FetchEntry,
|
|
6
|
+
ProvideEntry,
|
|
7
|
+
InjectEntry,
|
|
8
|
+
ComposableEntry,
|
|
9
|
+
RenderEntry,
|
|
10
|
+
TransitionEntry,
|
|
11
|
+
TraceEntry,
|
|
12
|
+
} from '@observatory/types/snapshot'
|
|
5
13
|
|
|
6
14
|
type ProvideInjectSnapshot = { provides: ProvideEntry[]; injects: InjectEntry[] }
|
|
7
15
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
editComposableValue,
|
|
7
7
|
openInEditor as openInEditorFromStore,
|
|
8
8
|
} from '@observatory-client/stores/observatory'
|
|
9
|
+
import { matchesComposableEntryQuery } from '@observatory-client/composables/composable-search'
|
|
9
10
|
import type { ComposableEntry as RuntimeComposableEntry } from '@observatory/types/snapshot'
|
|
10
11
|
|
|
11
12
|
const { composables: rawEntries, connected, features, clearComposables } = useObservatoryData()
|
|
@@ -118,23 +119,8 @@ const filtered = computed(() => {
|
|
|
118
119
|
return false
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (q) {
|
|
124
|
-
const matchesName = entry.name.toLowerCase().includes(q)
|
|
125
|
-
const matchesFile = entry.componentFile.toLowerCase().includes(q)
|
|
126
|
-
const matchesRef = Object.keys(entry.refs).some((k) => k.toLowerCase().includes(q))
|
|
127
|
-
const matchesVal = Object.values(entry.refs).some((v) => {
|
|
128
|
-
try {
|
|
129
|
-
return String(JSON.stringify(v.value)).toLowerCase().includes(q)
|
|
130
|
-
} catch {
|
|
131
|
-
return false
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
if (!matchesName && !matchesFile && !matchesRef && !matchesVal) {
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
122
|
+
if (search.value.trim() && !matchesComposableEntryQuery(entry, search.value)) {
|
|
123
|
+
return false
|
|
138
124
|
}
|
|
139
125
|
|
|
140
126
|
return true
|
|
@@ -234,22 +220,67 @@ function toggleRefExpand(entryId: string, refKey: string) {
|
|
|
234
220
|
}
|
|
235
221
|
|
|
236
222
|
// ── Reverse lookup ────────────────────────────────────────────────────────
|
|
237
|
-
// Clicking a ref key
|
|
223
|
+
// Clicking a ref key prefers identity-based lookup for shared/global refs.
|
|
224
|
+
// For non-shared keys, fallback to legacy key-name lookup.
|
|
238
225
|
|
|
239
|
-
|
|
226
|
+
interface LookupTarget {
|
|
227
|
+
key: string
|
|
228
|
+
composableName: string
|
|
229
|
+
identityGroup?: string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const lookupTarget = ref<LookupTarget | null>(null)
|
|
240
233
|
|
|
241
234
|
const lookupResults = computed(() => {
|
|
242
|
-
if (!
|
|
235
|
+
if (!lookupTarget.value) {
|
|
243
236
|
return []
|
|
244
237
|
}
|
|
245
238
|
|
|
246
|
-
const
|
|
239
|
+
const target = lookupTarget.value
|
|
240
|
+
|
|
241
|
+
if (target.identityGroup) {
|
|
242
|
+
return entries.value.filter(
|
|
243
|
+
(entry) =>
|
|
244
|
+
entry.name === target.composableName &&
|
|
245
|
+
entry.sharedKeyGroups?.[target.key] === target.identityGroup &&
|
|
246
|
+
target.key in entry.refs
|
|
247
|
+
)
|
|
248
|
+
}
|
|
247
249
|
|
|
248
|
-
return entries.value.filter((
|
|
250
|
+
return entries.value.filter((entry) => target.key in entry.refs)
|
|
249
251
|
})
|
|
250
252
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
+
const lookupTitle = computed(() => {
|
|
254
|
+
if (!lookupTarget.value) {
|
|
255
|
+
return ''
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (lookupTarget.value.identityGroup) {
|
|
259
|
+
return `${lookupTarget.value.key} (shared identity)`
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return lookupTarget.value.key
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
function openLookup(entry: RuntimeComposableEntry, key: string) {
|
|
266
|
+
const identityGroup = entry.sharedKeyGroups?.[key]
|
|
267
|
+
const next: LookupTarget = {
|
|
268
|
+
key,
|
|
269
|
+
composableName: entry.name,
|
|
270
|
+
identityGroup,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
lookupTarget.value?.key === next.key &&
|
|
275
|
+
lookupTarget.value?.composableName === next.composableName &&
|
|
276
|
+
lookupTarget.value?.identityGroup === next.identityGroup
|
|
277
|
+
) {
|
|
278
|
+
lookupTarget.value = null
|
|
279
|
+
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
lookupTarget.value = next
|
|
253
284
|
}
|
|
254
285
|
|
|
255
286
|
// ── Inline editing ────────────────────────────────────────────────────────
|
|
@@ -408,8 +439,12 @@ function applyEdit() {
|
|
|
408
439
|
<div v-for="[k, v] in Object.entries(entry.refs)" :key="k" class="composable-tracker__ref-row">
|
|
409
440
|
<span
|
|
410
441
|
class="composable-tracker__ref-key composable-tracker__ref-key--clickable mono text-sm"
|
|
411
|
-
:title="
|
|
412
|
-
|
|
442
|
+
:title="
|
|
443
|
+
entry.sharedKeyGroups?.[k]
|
|
444
|
+
? `click to see instances sharing this exact '${k}' state`
|
|
445
|
+
: `click to see all instances exposing '${k}'`
|
|
446
|
+
"
|
|
447
|
+
@click.stop="openLookup(entry, k)"
|
|
413
448
|
>
|
|
414
449
|
{{ k }}
|
|
415
450
|
</span>
|
|
@@ -530,14 +565,14 @@ function applyEdit() {
|
|
|
530
565
|
|
|
531
566
|
<!-- ── Reverse lookup panel ──────────────────────────────────────── -->
|
|
532
567
|
<Transition name="slide">
|
|
533
|
-
<div v-if="
|
|
568
|
+
<div v-if="lookupTarget" class="composable-tracker__lookup-panel">
|
|
534
569
|
<div class="composable-tracker__lookup-header">
|
|
535
|
-
<span class="mono text-sm">{{
|
|
570
|
+
<span class="mono text-sm">{{ lookupTitle }}</span>
|
|
536
571
|
<span class="muted text-sm">— {{ lookupResults.length }} instance{{ lookupResults.length !== 1 ? 's' : '' }}</span>
|
|
537
|
-
<button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="
|
|
572
|
+
<button class="composable-tracker__clear-btn composable-tracker__lookup-close" @click="lookupTarget = null">✕</button>
|
|
538
573
|
</div>
|
|
539
574
|
<div v-if="!lookupResults.length" class="composable-tracker__lookup-empty muted text-sm">
|
|
540
|
-
No
|
|
575
|
+
No instances matched this lookup.
|
|
541
576
|
</div>
|
|
542
577
|
<div v-for="r in lookupResults" :key="r.id" class="composable-tracker__lookup-row">
|
|
543
578
|
<span class="mono text-sm">{{ r.name }}</span>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { computed, defineComponent, h, ref, watch, type VNode } from 'vue'
|
|
3
3
|
import { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
4
|
import { useObservatoryData, openInEditor as openInEditorFromStore } from '@observatory-client/stores/observatory'
|
|
5
|
+
import { exportJson, importJson } from '@observatory-client/composables/useExportImport'
|
|
6
|
+
import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
|
|
5
7
|
import type { RenderEntry, RenderEvent } from '@observatory/types/snapshot'
|
|
6
8
|
|
|
7
9
|
interface ComponentNode {
|
|
@@ -197,6 +199,7 @@ const activeThreshold = computed({
|
|
|
197
199
|
})
|
|
198
200
|
const activeHotOnly = ref(false)
|
|
199
201
|
const frozen = ref(false)
|
|
202
|
+
const isImportedSnapshot = ref(false)
|
|
200
203
|
const search = ref('')
|
|
201
204
|
const activeSelectedId = ref<string | null>(null)
|
|
202
205
|
const activeRootId = ref<string | null>(null)
|
|
@@ -671,6 +674,7 @@ function updateSearch(event: Event) {
|
|
|
671
674
|
function toggleFreeze() {
|
|
672
675
|
if (frozen.value) {
|
|
673
676
|
frozen.value = false
|
|
677
|
+
isImportedSnapshot.value = false
|
|
674
678
|
frozenSnapshot.value = []
|
|
675
679
|
|
|
676
680
|
return
|
|
@@ -680,6 +684,45 @@ function toggleFreeze() {
|
|
|
680
684
|
frozen.value = true
|
|
681
685
|
}
|
|
682
686
|
|
|
687
|
+
function handleExport() {
|
|
688
|
+
exportJson(`observatory-renders-${Date.now()}.json`, {
|
|
689
|
+
type: 'observatory-renders',
|
|
690
|
+
version: '1',
|
|
691
|
+
exportedAt: Date.now(),
|
|
692
|
+
count: displayEntries.value.length,
|
|
693
|
+
data: displayEntries.value,
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function handleImport() {
|
|
698
|
+
let parsed: unknown
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
parsed = await importJson()
|
|
702
|
+
} catch (err) {
|
|
703
|
+
if (err instanceof Error && err.message !== 'cancelled') {
|
|
704
|
+
alert(`Import failed: ${err.message}`)
|
|
705
|
+
}
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const file = parsed as ObservatoryExportFile<RenderEntry>
|
|
710
|
+
|
|
711
|
+
if (
|
|
712
|
+
file?.type !== 'observatory-renders' ||
|
|
713
|
+
file?.version !== '1' ||
|
|
714
|
+
!Array.isArray(file?.data) ||
|
|
715
|
+
(file.data.length > 0 && (file.data[0]?.uid === undefined || !file.data[0]?.name || !file.data[0]?.file))
|
|
716
|
+
) {
|
|
717
|
+
alert('Invalid observatory renders file.')
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
frozenSnapshot.value = file.data
|
|
722
|
+
frozen.value = true
|
|
723
|
+
isImportedSnapshot.value = true
|
|
724
|
+
}
|
|
725
|
+
|
|
683
726
|
function basename(file: string) {
|
|
684
727
|
return (
|
|
685
728
|
file
|
|
@@ -732,8 +775,10 @@ function formatTimestamp(t: number): string {
|
|
|
732
775
|
<option v-for="r in knownRoutes" :key="r" :value="r">{{ r }}</option>
|
|
733
776
|
</select>
|
|
734
777
|
<button :class="{ active: frozen }" class="render-heatmap__freeze tracker-toolbar__spacer" @click="toggleFreeze">
|
|
735
|
-
{{ frozen ? 'unfreeze' : 'freeze snapshot' }}
|
|
778
|
+
{{ frozen && isImportedSnapshot ? 'unfreeze (imported)' : frozen ? 'unfreeze' : 'freeze snapshot' }}
|
|
736
779
|
</button>
|
|
780
|
+
<button class="render-heatmap__action-btn" title="Export render data as JSON" @click="handleExport">↓ export</button>
|
|
781
|
+
<button class="render-heatmap__action-btn" title="Import render data from JSON file" @click="handleImport">↑ import</button>
|
|
737
782
|
</div>
|
|
738
783
|
|
|
739
784
|
<div class="render-heatmap__stats tracker-stats-row">
|
|
@@ -952,6 +997,23 @@ function formatTimestamp(t: number): string {
|
|
|
952
997
|
width: 90px;
|
|
953
998
|
}
|
|
954
999
|
|
|
1000
|
+
.render-heatmap__action-btn {
|
|
1001
|
+
padding: 3px 8px;
|
|
1002
|
+
background: none;
|
|
1003
|
+
border: 1px solid var(--border);
|
|
1004
|
+
color: var(--text-secondary);
|
|
1005
|
+
cursor: pointer;
|
|
1006
|
+
font-size: 11px;
|
|
1007
|
+
border-radius: 3px;
|
|
1008
|
+
transition: all 0.12s;
|
|
1009
|
+
font-family: var(--mono);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.render-heatmap__action-btn:hover {
|
|
1013
|
+
background: var(--bg-secondary);
|
|
1014
|
+
color: var(--text);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
955
1017
|
.stat-sub {
|
|
956
1018
|
margin-top: var(--tracker-space-1);
|
|
957
1019
|
font-size: var(--tracker-font-size-sm);
|