nuxt-devtools-observatory 0.1.31 → 0.1.33
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 +79 -46
- package/client/.env.example +1 -0
- package/client/dist/assets/index-BqKYgjVB.js +20 -0
- package/client/dist/assets/index-bs1JBJ2u.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +7 -8
- package/client/src/components/SpanInspector.vue +1 -1
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- package/client/src/composables/composable-search.ts +127 -0
- package/client/src/composables/trace-render-aggregation.ts +263 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +1 -5
- package/client/src/composables/useVirtualizationConfig.ts +40 -0
- package/client/src/composables/useVirtualizationFlags.ts +129 -0
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +273 -97
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/ProvideInjectGraph.vue +41 -18
- package/client/src/views/RenderHeatmap.vue +392 -76
- package/client/src/views/TraceViewer.vue +797 -14
- package/client/src/views/TransitionTimeline.vue +112 -19
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -23
- 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 +23 -13
- package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
- package/dist/runtime/instrumentation/fetch.d.ts +7 -1
- package/dist/runtime/instrumentation/fetch.js +22 -1
- 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 +48 -1
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +86 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +18 -3
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.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 { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
7
|
import { useObservatoryData } from '@observatory-client/stores/observatory'
|
|
5
8
|
import type { TransitionEntry } from '@observatory/types/snapshot'
|
|
@@ -10,10 +13,25 @@ const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(260, 'obs
|
|
|
10
13
|
type FilterMode = 'all' | 'cancelled' | 'active' | 'completed'
|
|
11
14
|
const filter = ref<FilterMode>('all')
|
|
12
15
|
const search = ref('')
|
|
13
|
-
const
|
|
16
|
+
const selectedId = ref<string | null>(null)
|
|
17
|
+
const tableScrollRef = ref<HTMLElement | null>(null)
|
|
18
|
+
|
|
19
|
+
const { effective: virtualizationFlags } = useVirtualizationFlags()
|
|
20
|
+
const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 42, overscan: 6 })
|
|
21
|
+
|
|
22
|
+
const entriesSorted = computed(() => [...entries.value].sort((a, b) => a.startTime - b.startTime))
|
|
23
|
+
const entriesById = computed(() => new Map(entries.value.map((entry) => [entry.id, entry] as const)))
|
|
24
|
+
|
|
25
|
+
const selected = computed(() => {
|
|
26
|
+
if (!selectedId.value) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return entriesById.value.get(selectedId.value) ?? null
|
|
31
|
+
})
|
|
14
32
|
|
|
15
33
|
const filtered = computed(() => {
|
|
16
|
-
let list =
|
|
34
|
+
let list = entriesSorted.value
|
|
17
35
|
|
|
18
36
|
if (search.value) {
|
|
19
37
|
const q = search.value.toLowerCase()
|
|
@@ -28,7 +46,7 @@ const filtered = computed(() => {
|
|
|
28
46
|
list = list.filter((e) => e.phase === 'entered' || e.phase === 'left')
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
return list
|
|
49
|
+
return list
|
|
32
50
|
})
|
|
33
51
|
|
|
34
52
|
const stats = computed(() => ({
|
|
@@ -64,6 +82,60 @@ const timelineGeometry = computed(() => {
|
|
|
64
82
|
}))
|
|
65
83
|
})
|
|
66
84
|
|
|
85
|
+
const virtualizedRowsEnabled = computed(() => virtualizationFlags.value.transitions)
|
|
86
|
+
|
|
87
|
+
const tableVirtualizerOptions = computed(() => ({
|
|
88
|
+
count: filtered.value.length,
|
|
89
|
+
getScrollElement: () => tableScrollRef.value,
|
|
90
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
91
|
+
overscan: virtualizationPreset.value.overscan,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
const tableVirtualizer = useVirtualizer(tableVirtualizerOptions)
|
|
95
|
+
|
|
96
|
+
const tableVirtualItems = computed(() => {
|
|
97
|
+
if (!virtualizedRowsEnabled.value) {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return tableVirtualizer.value.getVirtualItems()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const topTablePadding = computed(() => {
|
|
105
|
+
if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return tableVirtualItems.value[0].start
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const bottomTablePadding = computed(() => {
|
|
113
|
+
if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const total = tableVirtualizer.value.getTotalSize()
|
|
118
|
+
const last = tableVirtualItems.value[tableVirtualItems.value.length - 1]
|
|
119
|
+
|
|
120
|
+
return Math.max(0, total - last.end)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const visibleRows = computed(() => {
|
|
124
|
+
if (!virtualizedRowsEnabled.value) {
|
|
125
|
+
return filtered.value.map((entry, index) => ({
|
|
126
|
+
entry,
|
|
127
|
+
geometry: timelineGeometry.value[index],
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return tableVirtualItems.value
|
|
132
|
+
.map((item) => ({
|
|
133
|
+
entry: filtered.value[item.index],
|
|
134
|
+
geometry: timelineGeometry.value[item.index],
|
|
135
|
+
}))
|
|
136
|
+
.filter((row): row is { entry: TransitionEntry; geometry: { left: number; width: number } } => Boolean(row.entry && row.geometry))
|
|
137
|
+
})
|
|
138
|
+
|
|
67
139
|
function phaseColor(phase: TransitionEntry['phase']): string {
|
|
68
140
|
if (phase === 'entering' || phase === 'leaving') {
|
|
69
141
|
return '#7f77dd'
|
|
@@ -166,7 +238,7 @@ function directionColor(e: TransitionEntry): string {
|
|
|
166
238
|
<!-- Main content -->
|
|
167
239
|
<div class="transition-timeline__content tracker-split">
|
|
168
240
|
<!-- Timeline table -->
|
|
169
|
-
<div class="transition-timeline__table tracker-table-wrap">
|
|
241
|
+
<div ref="tableScrollRef" class="transition-timeline__table tracker-table-wrap">
|
|
170
242
|
<table class="data-table">
|
|
171
243
|
<thead>
|
|
172
244
|
<tr>
|
|
@@ -180,41 +252,56 @@ function directionColor(e: TransitionEntry): string {
|
|
|
180
252
|
</thead>
|
|
181
253
|
<tbody>
|
|
182
254
|
<tr
|
|
183
|
-
v-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
255
|
+
v-if="virtualizedRowsEnabled && topTablePadding > 0"
|
|
256
|
+
class="transition-timeline__virtual-spacer-row"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<td colspan="6" :style="{ height: `${topTablePadding}px` }"></td>
|
|
260
|
+
</tr>
|
|
261
|
+
<tr
|
|
262
|
+
v-for="row in visibleRows"
|
|
263
|
+
:key="row.entry.id"
|
|
264
|
+
:class="{ selected: selected?.id === row.entry.id }"
|
|
265
|
+
@click="selectedId = selected?.id === row.entry.id ? null : row.entry.id"
|
|
187
266
|
>
|
|
188
267
|
<td>
|
|
189
|
-
<span class="transition-timeline__name mono">{{ entry.transitionName }}</span>
|
|
268
|
+
<span class="transition-timeline__name mono">{{ row.entry.transitionName }}</span>
|
|
190
269
|
</td>
|
|
191
270
|
<td>
|
|
192
|
-
<span class="transition-timeline__direction mono" :style="{ color: directionColor(entry) }">
|
|
193
|
-
{{ directionLabel(entry) }}
|
|
271
|
+
<span class="transition-timeline__direction mono" :style="{ color: directionColor(row.entry) }">
|
|
272
|
+
{{ directionLabel(row.entry) }}
|
|
194
273
|
</span>
|
|
195
274
|
</td>
|
|
196
275
|
<td>
|
|
197
|
-
<span class="badge" :class="phaseBadgeClass(entry.phase)">{{ entry.phase }}</span>
|
|
276
|
+
<span class="badge" :class="phaseBadgeClass(row.entry.phase)">{{ row.entry.phase }}</span>
|
|
198
277
|
</td>
|
|
199
278
|
<td class="transition-timeline__duration mono">
|
|
200
|
-
{{ entry.durationMs !== undefined ? entry.durationMs + 'ms' : '—' }}
|
|
279
|
+
{{ row.entry.durationMs !== undefined ? row.entry.durationMs + 'ms' : '—' }}
|
|
201
280
|
</td>
|
|
202
|
-
<td class="transition-timeline__component muted">{{ entry.parentComponent }}</td>
|
|
281
|
+
<td class="transition-timeline__component muted">{{ row.entry.parentComponent }}</td>
|
|
203
282
|
<td class="transition-timeline__bar-cell">
|
|
204
283
|
<div class="transition-timeline__bar-track">
|
|
205
284
|
<div
|
|
206
285
|
class="transition-timeline__bar-fill"
|
|
207
286
|
:style="{
|
|
208
|
-
left:
|
|
209
|
-
width: Math.max(
|
|
210
|
-
background: phaseColor(entry.phase),
|
|
211
|
-
opacity: entry.phase === 'entering' || entry.phase === 'leaving' ? '0.55' : '1',
|
|
287
|
+
left: row.geometry.left + '%',
|
|
288
|
+
width: Math.max(row.geometry.width, 1) + '%',
|
|
289
|
+
background: phaseColor(row.entry.phase),
|
|
290
|
+
opacity: row.entry.phase === 'entering' || row.entry.phase === 'leaving' ? '0.55' : '1',
|
|
212
291
|
}"
|
|
213
292
|
/>
|
|
214
293
|
</div>
|
|
215
294
|
</td>
|
|
216
295
|
</tr>
|
|
217
296
|
|
|
297
|
+
<tr
|
|
298
|
+
v-if="virtualizedRowsEnabled && bottomTablePadding > 0"
|
|
299
|
+
class="transition-timeline__virtual-spacer-row"
|
|
300
|
+
aria-hidden="true"
|
|
301
|
+
>
|
|
302
|
+
<td colspan="6" :style="{ height: `${bottomTablePadding}px` }"></td>
|
|
303
|
+
</tr>
|
|
304
|
+
|
|
218
305
|
<tr v-if="!filtered.length">
|
|
219
306
|
<td colspan="6" class="tracker-empty-cell">
|
|
220
307
|
{{
|
|
@@ -236,7 +323,7 @@ function directionColor(e: TransitionEntry): string {
|
|
|
236
323
|
<aside v-if="selected" class="transition-timeline__detail" :style="{ width: detailWidth + 'px' }">
|
|
237
324
|
<div class="transition-timeline__detail-header">
|
|
238
325
|
<span class="transition-timeline__detail-title">{{ selected.transitionName }}</span>
|
|
239
|
-
<button class="transition-timeline__close-btn" @click="
|
|
326
|
+
<button class="transition-timeline__close-btn" @click="selectedId = null">✕</button>
|
|
240
327
|
</div>
|
|
241
328
|
|
|
242
329
|
<div class="transition-timeline__detail-section">
|
|
@@ -392,6 +479,12 @@ function directionColor(e: TransitionEntry): string {
|
|
|
392
479
|
border-radius: 0;
|
|
393
480
|
}
|
|
394
481
|
|
|
482
|
+
.transition-timeline__virtual-spacer-row td {
|
|
483
|
+
padding: 0;
|
|
484
|
+
border-bottom: 0;
|
|
485
|
+
background: transparent;
|
|
486
|
+
}
|
|
487
|
+
|
|
395
488
|
/* ── Timeline bar ────────────────────────────────────────────────────────── */
|
|
396
489
|
.transition-timeline__bar-cell {
|
|
397
490
|
width: 200px;
|
package/dist/module.d.mts
CHANGED
|
@@ -19,6 +19,11 @@ interface ModuleOptions {
|
|
|
19
19
|
* @default 10000
|
|
20
20
|
*/
|
|
21
21
|
maxPayloadBytes?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Number of fetch rows to load per infinite-scroll step in the Fetch Dashboard.
|
|
24
|
+
* @default 20
|
|
25
|
+
*/
|
|
26
|
+
fetchPageSize?: number;
|
|
22
27
|
/**
|
|
23
28
|
* Maximum number of transition entries to keep in memory
|
|
24
29
|
* @default 500
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addVitePlugin, addPlugin, addServerPlugin } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, addVitePlugin, addImports, addPlugin, addServerPlugin } from '@nuxt/kit';
|
|
2
2
|
import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
|
|
3
3
|
import sirv from 'sirv';
|
|
4
4
|
import { parse } from '@babel/parser';
|
|
@@ -209,10 +209,6 @@ function fetchInstrumentPlugin() {
|
|
|
209
209
|
plugins: ["typescript"]
|
|
210
210
|
});
|
|
211
211
|
let modified = false;
|
|
212
|
-
let needsFetchCallHelper = false;
|
|
213
|
-
let needsTracedAsyncDataHelper = false;
|
|
214
|
-
const hasFetchCallImport = scriptCode.includes("__devFetchCall");
|
|
215
|
-
const hasTracedAsyncDataImport = scriptCode.includes("useTracedAsyncData");
|
|
216
212
|
traverse$1(ast, {
|
|
217
213
|
CallExpression(path) {
|
|
218
214
|
if (path.node.__observatoryTransformed) {
|
|
@@ -284,7 +280,6 @@ function fetchInstrumentPlugin() {
|
|
|
284
280
|
]);
|
|
285
281
|
newCall.__observatoryTransformed = true;
|
|
286
282
|
path.replaceWith(newCall);
|
|
287
|
-
needsTracedAsyncDataHelper = true;
|
|
288
283
|
modified = true;
|
|
289
284
|
} else {
|
|
290
285
|
const newCall = t.callExpression(t.identifier("__devFetchCall"), [
|
|
@@ -294,7 +289,6 @@ function fetchInstrumentPlugin() {
|
|
|
294
289
|
meta
|
|
295
290
|
]);
|
|
296
291
|
newCall.__observatoryTransformed = true;
|
|
297
|
-
needsFetchCallHelper = true;
|
|
298
292
|
path.replaceWith(newCall);
|
|
299
293
|
modified = true;
|
|
300
294
|
}
|
|
@@ -303,18 +297,12 @@ function fetchInstrumentPlugin() {
|
|
|
303
297
|
if (!modified) {
|
|
304
298
|
return null;
|
|
305
299
|
}
|
|
306
|
-
const fetchImportNames = [needsFetchCallHelper && !hasFetchCallImport ? "__devFetchCall" : ""].filter(Boolean);
|
|
307
|
-
const fetchImportStatement = fetchImportNames.length ? `import { ${fetchImportNames.join(", ")} } from 'nuxt-devtools-observatory/runtime/fetch-registry';
|
|
308
|
-
` : "";
|
|
309
|
-
const asyncDataImportStatement = needsTracedAsyncDataHelper && !hasTracedAsyncDataImport ? `import { useTracedAsyncData } from 'nuxt-devtools-observatory/runtime/async-data-instrumentation';
|
|
310
|
-
` : "";
|
|
311
|
-
const importStatement = fetchImportStatement + asyncDataImportStatement;
|
|
312
300
|
const output = generate$1(ast, { retainLines: true }, scriptCode);
|
|
313
301
|
let finalCode;
|
|
314
302
|
if (isVue) {
|
|
315
|
-
finalCode = code.slice(0, scriptStart) +
|
|
303
|
+
finalCode = code.slice(0, scriptStart) + output.code + code.slice(scriptStart + scriptCode.length);
|
|
316
304
|
} else {
|
|
317
|
-
finalCode =
|
|
305
|
+
finalCode = output.code;
|
|
318
306
|
}
|
|
319
307
|
return {
|
|
320
308
|
code: finalCode,
|
|
@@ -537,6 +525,7 @@ const defaults = {
|
|
|
537
525
|
heatmapThresholdTime: process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600,
|
|
538
526
|
maxFetchEntries: process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200,
|
|
539
527
|
maxPayloadBytes: process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4,
|
|
528
|
+
fetchPageSize: process.env.OBSERVATORY_FETCH_PAGE_SIZE ? Number(process.env.OBSERVATORY_FETCH_PAGE_SIZE) : 20,
|
|
540
529
|
maxTransitions: process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500,
|
|
541
530
|
maxComposableHistory: process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50,
|
|
542
531
|
maxComposableEntries: process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300,
|
|
@@ -576,6 +565,7 @@ const module$1 = defineNuxtModule({
|
|
|
576
565
|
heatmapThresholdTime: options.heatmapThresholdTime ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600),
|
|
577
566
|
maxFetchEntries: options.maxFetchEntries ?? (process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200),
|
|
578
567
|
maxPayloadBytes: options.maxPayloadBytes ?? (process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4),
|
|
568
|
+
fetchPageSize: options.fetchPageSize ?? (process.env.OBSERVATORY_FETCH_PAGE_SIZE ? Number(process.env.OBSERVATORY_FETCH_PAGE_SIZE) : 20),
|
|
579
569
|
maxTransitions: options.maxTransitions ?? (process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500),
|
|
580
570
|
maxComposableHistory: options.maxComposableHistory ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50),
|
|
581
571
|
maxComposableEntries: options.maxComposableEntries ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300),
|
|
@@ -599,6 +589,10 @@ const module$1 = defineNuxtModule({
|
|
|
599
589
|
const vitePluginScope = resolved.instrumentServer ? { server: true, client: true } : { server: false, client: true };
|
|
600
590
|
if (resolved.fetchDashboard) {
|
|
601
591
|
addVitePlugin(fetchInstrumentPlugin(), vitePluginScope);
|
|
592
|
+
addImports([
|
|
593
|
+
{ name: "__devFetchCall", from: resolver.resolve("./runtime/composables/fetch-registry") },
|
|
594
|
+
{ name: "useTracedAsyncData", from: resolver.resolve("./runtime/instrumentation/asyncData") }
|
|
595
|
+
]);
|
|
602
596
|
}
|
|
603
597
|
if (resolved.provideInjectGraph) {
|
|
604
598
|
addVitePlugin(provideInjectPlugin(), vitePluginScope);
|
|
@@ -612,7 +606,7 @@ const module$1 = defineNuxtModule({
|
|
|
612
606
|
if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
|
|
613
607
|
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
614
608
|
}
|
|
615
|
-
if (resolved.fetchDashboard) {
|
|
609
|
+
if (resolved.fetchDashboard || resolved.traceViewer && resolved.instrumentServer) {
|
|
616
610
|
addServerPlugin(resolver.resolve("./runtime/nitro/fetch-capture"));
|
|
617
611
|
}
|
|
618
612
|
const base = "/__observatory";
|
|
@@ -634,6 +628,7 @@ const module$1 = defineNuxtModule({
|
|
|
634
628
|
provideInjectGraph: !!resolved.provideInjectGraph,
|
|
635
629
|
composableTracker: !!resolved.composableTracker,
|
|
636
630
|
composableNavigationMode: resolved.composableNavigationMode,
|
|
631
|
+
fetchPageSize: resolved.fetchPageSize,
|
|
637
632
|
renderHeatmap: !!resolved.renderHeatmap,
|
|
638
633
|
transitionTracker: !!resolved.transitionTracker,
|
|
639
634
|
traceViewer: !!resolved.traceViewer
|
|
@@ -691,13 +686,6 @@ const module$1 = defineNuxtModule({
|
|
|
691
686
|
);
|
|
692
687
|
rpc.broadcast.onSnapshot.asEvent(latestSnapshot);
|
|
693
688
|
}, nuxt);
|
|
694
|
-
nuxt.hook("render:response", (response, { url }) => {
|
|
695
|
-
if (url.startsWith("/trackers") || url === "/" || url.startsWith("/index.html")) {
|
|
696
|
-
const configScript = `<script>window.__observatoryConfig = ${JSON.stringify(nuxt.options.runtimeConfig.public.observatory)};<\/script>`;
|
|
697
|
-
response.body = response.body.replace("<head>", `<head>
|
|
698
|
-
${configScript}`);
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
689
|
nuxt.hook("devtools:customTabs", (tabs) => {
|
|
702
690
|
if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
|
|
703
691
|
tabs.push({
|
|
@@ -718,6 +706,7 @@ ${configScript}`);
|
|
|
718
706
|
traceViewer: resolved.traceViewer,
|
|
719
707
|
maxFetchEntries: resolved.maxFetchEntries,
|
|
720
708
|
maxPayloadBytes: resolved.maxPayloadBytes,
|
|
709
|
+
fetchPageSize: resolved.fetchPageSize,
|
|
721
710
|
maxTransitions: resolved.maxTransitions,
|
|
722
711
|
maxComposableHistory: resolved.maxComposableHistory,
|
|
723
712
|
maxComposableEntries: resolved.maxComposableEntries,
|
|
@@ -22,6 +22,11 @@ export interface ComposableEntry {
|
|
|
22
22
|
* instances of this composable — indicates module-level (global) state.
|
|
23
23
|
*/
|
|
24
24
|
sharedKeys: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Stable per-composable-name identity group id for each shared key.
|
|
27
|
+
* Keys are ref/reactive key names; values are group ids like `group-1`.
|
|
28
|
+
*/
|
|
29
|
+
sharedKeyGroups?: Record<string, string>;
|
|
25
30
|
watcherCount: number;
|
|
26
31
|
intervalCount: number;
|
|
27
32
|
lifecycle: {
|
|
@@ -39,6 +44,19 @@ export interface ComposableEntry {
|
|
|
39
44
|
/** Whether this composable is called from a layout component (persists across pages). */
|
|
40
45
|
isLayoutComposable?: boolean;
|
|
41
46
|
}
|
|
47
|
+
interface SsrObservatoryEvent {
|
|
48
|
+
context?: {
|
|
49
|
+
__observatoryRequestId?: string;
|
|
50
|
+
__ssrFetchStart?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function __recordSsrComposableSpan(name: string, meta: {
|
|
54
|
+
file: string;
|
|
55
|
+
line: number;
|
|
56
|
+
}, startTime: number, endTime: number, opts?: {
|
|
57
|
+
error?: unknown;
|
|
58
|
+
event?: SsrObservatoryEvent;
|
|
59
|
+
}): void;
|
|
42
60
|
/**
|
|
43
61
|
* Registers a new composable entry, updates an existing one, or retrieves all entries.
|
|
44
62
|
* @remarks The returned object exposes the following methods:
|
|
@@ -78,3 +96,4 @@ export declare function __trackComposable<T>(name: string, callFn: () => T, meta
|
|
|
78
96
|
file: string;
|
|
79
97
|
line: number;
|
|
80
98
|
}): T;
|
|
99
|
+
export {};
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
|
|
2
|
+
import { addSsrPhaseSpan } from "../nitro/ssr-trace-store.js";
|
|
3
|
+
function nowMs() {
|
|
4
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
5
|
+
}
|
|
6
|
+
export function __recordSsrComposableSpan(name, meta, startTime, endTime, opts = {}) {
|
|
7
|
+
const eventContext = opts.event?.context ?? globalThis.__observatorySsrContext__;
|
|
8
|
+
const requestId = eventContext?.__observatoryRequestId;
|
|
9
|
+
const requestStart = eventContext?.__ssrFetchStart;
|
|
10
|
+
if (!requestId || typeof requestStart !== "number") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const metadata = {
|
|
14
|
+
file: meta.file,
|
|
15
|
+
line: meta.line,
|
|
16
|
+
phase: "setup"
|
|
17
|
+
};
|
|
18
|
+
if (opts.error instanceof Error) {
|
|
19
|
+
metadata.errorMessage = opts.error.message;
|
|
20
|
+
}
|
|
21
|
+
addSsrPhaseSpan(requestId, {
|
|
22
|
+
name: `composable:${name}`,
|
|
23
|
+
type: "composable",
|
|
24
|
+
startMs: Math.max(startTime - requestStart, 0),
|
|
25
|
+
endMs: Math.max(endTime - requestStart, 0),
|
|
26
|
+
error: !!opts.error,
|
|
27
|
+
metadata
|
|
28
|
+
});
|
|
29
|
+
}
|
|
2
30
|
export function setupComposableRegistry() {
|
|
3
31
|
const entries = /* @__PURE__ */ new Map();
|
|
4
32
|
const liveRefs = /* @__PURE__ */ new Map();
|
|
@@ -38,14 +66,30 @@ export function setupComposableRegistry() {
|
|
|
38
66
|
}
|
|
39
67
|
nameCache = /* @__PURE__ */ new Map();
|
|
40
68
|
sharedKeysCache.set(name, nameCache);
|
|
69
|
+
const identityIds = /* @__PURE__ */ new WeakMap();
|
|
70
|
+
let nextIdentity = 1;
|
|
71
|
+
const getIdentityGroup = (obj) => {
|
|
72
|
+
if (!obj || typeof obj !== "object") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
const target = obj;
|
|
76
|
+
const existing = identityIds.get(target);
|
|
77
|
+
if (existing) {
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
const created = `group-${nextIdentity++}`;
|
|
81
|
+
identityIds.set(target, created);
|
|
82
|
+
return created;
|
|
83
|
+
};
|
|
41
84
|
const peers = [...entries.entries()].filter(([, e]) => e.name === name);
|
|
42
85
|
for (const [eid] of peers) {
|
|
43
86
|
const ownRaw = rawRefs.get(eid);
|
|
44
87
|
if (!ownRaw) {
|
|
45
|
-
nameCache.set(eid, []);
|
|
88
|
+
nameCache.set(eid, { keys: [], groups: {} });
|
|
46
89
|
continue;
|
|
47
90
|
}
|
|
48
91
|
const shared = /* @__PURE__ */ new Set();
|
|
92
|
+
const groups = {};
|
|
49
93
|
for (const [otherId] of peers) {
|
|
50
94
|
if (otherId === eid) {
|
|
51
95
|
continue;
|
|
@@ -57,12 +101,16 @@ export function setupComposableRegistry() {
|
|
|
57
101
|
for (const [key, obj] of Object.entries(ownRaw)) {
|
|
58
102
|
if (key in otherRaw && otherRaw[key] === obj) {
|
|
59
103
|
shared.add(key);
|
|
104
|
+
const identity = getIdentityGroup(obj);
|
|
105
|
+
if (identity) {
|
|
106
|
+
groups[key] = identity;
|
|
107
|
+
}
|
|
60
108
|
}
|
|
61
109
|
}
|
|
62
110
|
}
|
|
63
|
-
nameCache.set(eid, [...shared]);
|
|
111
|
+
nameCache.set(eid, { keys: [...shared], groups });
|
|
64
112
|
}
|
|
65
|
-
return nameCache.get(id) ?? [];
|
|
113
|
+
return nameCache.get(id) ?? { keys: [], groups: {} };
|
|
66
114
|
}
|
|
67
115
|
let currentRoute = "/";
|
|
68
116
|
function setRoute(path) {
|
|
@@ -197,6 +245,7 @@ export function setupComposableRegistry() {
|
|
|
197
245
|
}
|
|
198
246
|
])
|
|
199
247
|
);
|
|
248
|
+
const shared = getSharedKeys(entry.id, entry.name);
|
|
200
249
|
return {
|
|
201
250
|
id: entry.id,
|
|
202
251
|
name: entry.name,
|
|
@@ -207,7 +256,8 @@ export function setupComposableRegistry() {
|
|
|
207
256
|
leakReason: entry.leakReason,
|
|
208
257
|
refs: freshRefs,
|
|
209
258
|
history: entryHistory.get(entry.id) ?? [],
|
|
210
|
-
sharedKeys:
|
|
259
|
+
sharedKeys: shared.keys,
|
|
260
|
+
sharedKeyGroups: shared.groups,
|
|
211
261
|
watcherCount: entry.watcherCount,
|
|
212
262
|
intervalCount: entry.intervalCount,
|
|
213
263
|
lifecycle: entry.lifecycle,
|
|
@@ -318,7 +368,15 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
318
368
|
return callFn();
|
|
319
369
|
}
|
|
320
370
|
if (!import.meta.client) {
|
|
321
|
-
|
|
371
|
+
const startTime = nowMs();
|
|
372
|
+
try {
|
|
373
|
+
const result2 = callFn();
|
|
374
|
+
__recordSsrComposableSpan(name, meta, startTime, nowMs());
|
|
375
|
+
return result2;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
__recordSsrComposableSpan(name, meta, startTime, nowMs(), { error });
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
322
380
|
}
|
|
323
381
|
const registry = window.__observatory__?.composable;
|
|
324
382
|
if (!registry) {
|
|
@@ -8,6 +8,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
8
8
|
const HIDE_INTERNALS = config.heatmapHideInternals ?? false;
|
|
9
9
|
let dirty = true;
|
|
10
10
|
let cachedSnapshot = "[]";
|
|
11
|
+
let resetTimestamp = 0;
|
|
11
12
|
const liveElements = /* @__PURE__ */ new Map();
|
|
12
13
|
function markDirty() {
|
|
13
14
|
dirty = true;
|
|
@@ -61,6 +62,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
function reset() {
|
|
65
|
+
resetTimestamp = performance.now();
|
|
64
66
|
for (const entry of entries.values()) {
|
|
65
67
|
entry.isPersistent = true;
|
|
66
68
|
entry.rerenders = 0;
|
|
@@ -72,23 +74,30 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
72
74
|
markDirty();
|
|
73
75
|
}
|
|
74
76
|
function aggregateFromComponentSpans() {
|
|
75
|
-
const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "component");
|
|
76
|
-
const
|
|
77
|
+
const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "render" || span.type === "component");
|
|
78
|
+
const allSpansByUid = /* @__PURE__ */ new Map();
|
|
79
|
+
const postResetSpansByUid = /* @__PURE__ */ new Map();
|
|
77
80
|
for (const span of componentSpans) {
|
|
78
81
|
const uidValue = span.metadata?.uid;
|
|
79
82
|
const uid = typeof uidValue === "number" ? uidValue : Number(uidValue);
|
|
80
83
|
if (!Number.isFinite(uid)) {
|
|
81
84
|
continue;
|
|
82
85
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
const allList = allSpansByUid.get(uid) ?? [];
|
|
87
|
+
allList.push(span);
|
|
88
|
+
allSpansByUid.set(uid, allList);
|
|
89
|
+
if (span.startTime >= resetTimestamp) {
|
|
90
|
+
const postList = postResetSpansByUid.get(uid) ?? [];
|
|
91
|
+
postList.push(span);
|
|
92
|
+
postResetSpansByUid.set(uid, postList);
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
for (const [uid, entry] of entries.entries()) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const timeline =
|
|
91
|
-
const
|
|
96
|
+
const allSpans = (allSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
|
|
97
|
+
const postResetSpans = (postResetSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
|
|
98
|
+
const timeline = postResetSpans.slice(-MAX_TIMELINE).map((span) => {
|
|
99
|
+
const isMountLifecycle = span.metadata?.lifecycle === "render:mount" || span.metadata?.lifecycle === "mounted";
|
|
100
|
+
const lifecycle = isMountLifecycle ? "mount" : "update";
|
|
92
101
|
const routeValue = span.metadata?.route;
|
|
93
102
|
const route = typeof routeValue === "string" && routeValue.length > 0 ? routeValue : entry.route;
|
|
94
103
|
return {
|
|
@@ -98,10 +107,11 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
98
107
|
route
|
|
99
108
|
};
|
|
100
109
|
});
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
110
|
+
const isMountSpan = (span) => span.metadata?.lifecycle === "render:mount" || span.metadata?.lifecycle === "mounted";
|
|
111
|
+
const mountCount = allSpans.filter(isMountSpan).length;
|
|
112
|
+
const rerenders = postResetSpans.filter((span) => !isMountSpan(span)).length;
|
|
113
|
+
const totalMs = postResetSpans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
|
|
114
|
+
const eventsCount = Math.max(postResetSpans.length, 1);
|
|
105
115
|
entry.mountCount = mountCount;
|
|
106
116
|
entry.rerenders = rerenders;
|
|
107
117
|
entry.totalMs = Math.round(totalMs * 10) / 10;
|
|
@@ -4,6 +4,6 @@ interface AsyncDataMeta {
|
|
|
4
4
|
line: number;
|
|
5
5
|
originalFn?: string;
|
|
6
6
|
}
|
|
7
|
-
type AnyFn = (...args:
|
|
7
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
8
8
|
export declare function useTracedAsyncData<TFn extends AnyFn>(originalFn: TFn, args: unknown[], handlerIndex: number, key: unknown, meta: AsyncDataMeta): ReturnType<TFn>;
|
|
9
9
|
export {};
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { NuxtApp } from '#app';
|
|
2
|
-
|
|
2
|
+
import type { FetchEntry } from '../composables/fetch-registry.js';
|
|
3
|
+
type FetchRegistry = {
|
|
4
|
+
register: (entry: FetchEntry) => void;
|
|
5
|
+
update: (id: string, patch: Partial<FetchEntry>) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function setupFetchInstrumentation(nuxtApp: NuxtApp, fetchRegistry?: FetchRegistry): void;
|
|
8
|
+
export {};
|
|
@@ -27,7 +27,7 @@ function resolveErrorStatus(error) {
|
|
|
27
27
|
return target?.response?.status ?? target?.statusCode ?? target?.status;
|
|
28
28
|
}
|
|
29
29
|
const WRAPPED_FETCH_FLAG = "__observatory_wrapped_fetch__";
|
|
30
|
-
export function setupFetchInstrumentation(nuxtApp) {
|
|
30
|
+
export function setupFetchInstrumentation(nuxtApp, fetchRegistry) {
|
|
31
31
|
const original = nuxtApp.$fetch;
|
|
32
32
|
if (!original) {
|
|
33
33
|
return;
|
|
@@ -39,6 +39,7 @@ export function setupFetchInstrumentation(nuxtApp) {
|
|
|
39
39
|
const url = resolveUrl(request);
|
|
40
40
|
const method = resolveMethod(request, options);
|
|
41
41
|
const startedAt = performance.now();
|
|
42
|
+
const entryId = `$fetch::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
|
|
42
43
|
const span = startSpan({
|
|
43
44
|
name: "$fetch",
|
|
44
45
|
type: "fetch",
|
|
@@ -49,6 +50,15 @@ export function setupFetchInstrumentation(nuxtApp) {
|
|
|
49
50
|
status: "pending"
|
|
50
51
|
}
|
|
51
52
|
});
|
|
53
|
+
fetchRegistry?.register({
|
|
54
|
+
id: entryId,
|
|
55
|
+
key: url,
|
|
56
|
+
url,
|
|
57
|
+
status: "pending",
|
|
58
|
+
origin: "csr",
|
|
59
|
+
startTime: startedAt,
|
|
60
|
+
cached: false
|
|
61
|
+
});
|
|
52
62
|
return Promise.resolve(original(request, options)).then((result) => {
|
|
53
63
|
const durationMs = Math.max(performance.now() - startedAt, 0);
|
|
54
64
|
span.end({
|
|
@@ -61,6 +71,11 @@ export function setupFetchInstrumentation(nuxtApp) {
|
|
|
61
71
|
durationMs: Math.round(durationMs * 10) / 10
|
|
62
72
|
}
|
|
63
73
|
});
|
|
74
|
+
fetchRegistry?.update(entryId, {
|
|
75
|
+
status: "ok",
|
|
76
|
+
endTime: performance.now(),
|
|
77
|
+
ms: Math.round(durationMs * 10) / 10
|
|
78
|
+
});
|
|
64
79
|
return result;
|
|
65
80
|
}).catch((error) => {
|
|
66
81
|
const durationMs = Math.max(performance.now() - startedAt, 0);
|
|
@@ -76,6 +91,12 @@ export function setupFetchInstrumentation(nuxtApp) {
|
|
|
76
91
|
durationMs: Math.round(durationMs * 10) / 10
|
|
77
92
|
}
|
|
78
93
|
});
|
|
94
|
+
fetchRegistry?.update(entryId, {
|
|
95
|
+
status: "error",
|
|
96
|
+
endTime: performance.now(),
|
|
97
|
+
ms: Math.round(durationMs * 10) / 10,
|
|
98
|
+
error
|
|
99
|
+
});
|
|
79
100
|
throw error;
|
|
80
101
|
});
|
|
81
102
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { H3Event } from 'h3';
|
|
2
1
|
interface NitroAppLike {
|
|
3
2
|
hooks: {
|
|
4
|
-
hook: (name:
|
|
3
|
+
hook: (name: string, handler: (...args: unknown[]) => void) => void;
|
|
5
4
|
};
|
|
6
5
|
}
|
|
7
6
|
export default function fetchCapturePlugin(nitroApp: NitroAppLike): void;
|