nuxt-devtools-observatory 0.1.30 → 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 +117 -30
- package/client/.env.example +2 -1
- 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/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +442 -0
- package/client/src/components/SpanInspector.vue +446 -0
- package/client/src/components/TraceFilter.vue +342 -0
- package/client/src/components/WaterfallView.vue +443 -0
- 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 +160 -0
- package/client/src/stores/observatory.ts +13 -1
- package/client/src/views/ComposableTracker.vue +65 -30
- package/client/src/views/RenderHeatmap.vue +63 -1
- package/client/src/views/TraceViewer.vue +1212 -0
- package/client/src/views/TransitionTimeline.vue +1 -6
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +31 -45
- 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 +74 -110
- package/dist/runtime/composables/transition-registry.js +103 -28
- package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
- package/dist/runtime/instrumentation/asyncData.js +49 -0
- package/dist/runtime/instrumentation/component.d.ts +2 -0
- package/dist/runtime/instrumentation/component.js +126 -0
- package/dist/runtime/instrumentation/fetch.d.ts +2 -0
- package/dist/runtime/instrumentation/fetch.js +89 -0
- package/dist/runtime/instrumentation/route.d.ts +6 -0
- package/dist/runtime/instrumentation/route.js +41 -0
- 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 +82 -2
- package/dist/runtime/tracing/context.d.ts +9 -0
- package/dist/runtime/tracing/context.js +15 -0
- package/dist/runtime/tracing/trace.d.ts +25 -0
- package/dist/runtime/tracing/trace.js +0 -0
- package/dist/runtime/tracing/traceStore.d.ts +39 -0
- package/dist/runtime/tracing/traceStore.js +101 -0
- package/dist/runtime/tracing/tracing.d.ts +27 -0
- package/dist/runtime/tracing/tracing.js +48 -0
- package/package.json +5 -1
- package/client/.env +0 -16
- package/client/dist/assets/index-BCaKoHBH.js +0 -17
- package/client/dist/assets/index-BmGW_M3W.css +0 -1
|
@@ -28,12 +28,7 @@ const filtered = computed(() => {
|
|
|
28
28
|
list = list.filter((e) => e.phase === 'entered' || e.phase === 'left')
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
return list.sort((a, b) =>
|
|
32
|
-
const aTime = a.endTime ?? a.startTime
|
|
33
|
-
const bTime = b.endTime ?? b.startTime
|
|
34
|
-
|
|
35
|
-
return bTime - aTime
|
|
36
|
-
})
|
|
31
|
+
return list.sort((a, b) => a.startTime - b.startTime)
|
|
37
32
|
})
|
|
38
33
|
|
|
39
34
|
const stats = computed(() => ({
|
package/dist/module.d.mts
CHANGED
|
@@ -71,6 +71,11 @@ interface ModuleOptions {
|
|
|
71
71
|
* @default true
|
|
72
72
|
*/
|
|
73
73
|
transitionTracker?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Enable the trace viewer tab (per-route component + fetch + composable + render spans)
|
|
76
|
+
* @default true
|
|
77
|
+
*/
|
|
78
|
+
traceViewer?: boolean;
|
|
74
79
|
/**
|
|
75
80
|
* Hide node_modules/internal components in the render heatmap
|
|
76
81
|
* @default false
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -187,7 +187,7 @@ function fetchInstrumentPlugin() {
|
|
|
187
187
|
if (!isVue && !id.endsWith(".ts") && !id.endsWith(".js")) {
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
-
if (id.includes("node_modules") || id.includes("composable-registry") || id.includes("provide-inject-registry") || id.includes("fetch-registry")) {
|
|
190
|
+
if (id.includes("node_modules") || id.includes("composable-registry") || id.includes("provide-inject-registry") || id.includes("fetch-registry") || id.includes("instrumentation/asyncData")) {
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
let scriptCode = code;
|
|
@@ -210,9 +210,9 @@ function fetchInstrumentPlugin() {
|
|
|
210
210
|
});
|
|
211
211
|
let modified = false;
|
|
212
212
|
let needsFetchCallHelper = false;
|
|
213
|
-
let
|
|
213
|
+
let needsTracedAsyncDataHelper = false;
|
|
214
214
|
const hasFetchCallImport = scriptCode.includes("__devFetchCall");
|
|
215
|
-
const
|
|
215
|
+
const hasTracedAsyncDataImport = scriptCode.includes("useTracedAsyncData");
|
|
216
216
|
traverse$1(ast, {
|
|
217
217
|
CallExpression(path) {
|
|
218
218
|
if (path.node.__observatoryTransformed) {
|
|
@@ -225,7 +225,7 @@ function fetchInstrumentPlugin() {
|
|
|
225
225
|
if (!FETCH_FNS.has(callee.name)) {
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
if (path.parent && t.isCallExpression(path.parent) && t.isIdentifier(path.parent.callee) && ["__devFetchCall", "
|
|
228
|
+
if (path.parent && t.isCallExpression(path.parent) && t.isIdentifier(path.parent.callee) && ["__devFetchCall", "useTracedAsyncData"].includes(path.parent.callee.name)) {
|
|
229
229
|
return;
|
|
230
230
|
}
|
|
231
231
|
const originalName = callee.name;
|
|
@@ -273,40 +273,18 @@ function fetchInstrumentPlugin() {
|
|
|
273
273
|
t.objectProperty(t.identifier("originalFn"), t.stringLiteral(originalName))
|
|
274
274
|
]);
|
|
275
275
|
if ((originalName === "useAsyncData" || originalName === "useLazyAsyncData") && handlerArg) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
meta
|
|
289
|
-
]),
|
|
290
|
-
[t.spreadElement(t.identifier("args"))]
|
|
291
|
-
),
|
|
292
|
-
t.callExpression(handlerArg, [t.spreadElement(t.identifier("args"))])
|
|
293
|
-
)
|
|
294
|
-
);
|
|
295
|
-
wrappedHandler.__observatoryTransformed = true;
|
|
296
|
-
needsFetchHandlerHelper = true;
|
|
297
|
-
if (keyArg) {
|
|
298
|
-
const newCall = t.callExpression(t.identifier(originalName), [
|
|
299
|
-
keyArg,
|
|
300
|
-
wrappedHandler,
|
|
301
|
-
optsArg ?? t.objectExpression([])
|
|
302
|
-
]);
|
|
303
|
-
newCall.__observatoryTransformed = true;
|
|
304
|
-
path.replaceWith(newCall);
|
|
305
|
-
} else {
|
|
306
|
-
const newCall = t.callExpression(t.identifier(originalName), [wrappedHandler]);
|
|
307
|
-
newCall.__observatoryTransformed = true;
|
|
308
|
-
path.replaceWith(newCall);
|
|
309
|
-
}
|
|
276
|
+
const rewrittenArgs = keyArg ? [keyArg, handlerArg, optsArg ?? t.objectExpression([])] : [handlerArg];
|
|
277
|
+
const handlerIndex = keyArg ? 1 : 0;
|
|
278
|
+
const newCall = t.callExpression(t.identifier("useTracedAsyncData"), [
|
|
279
|
+
t.identifier(originalName),
|
|
280
|
+
t.arrayExpression(rewrittenArgs),
|
|
281
|
+
t.numericLiteral(handlerIndex),
|
|
282
|
+
keyArg ?? t.stringLiteral(key),
|
|
283
|
+
meta
|
|
284
|
+
]);
|
|
285
|
+
newCall.__observatoryTransformed = true;
|
|
286
|
+
path.replaceWith(newCall);
|
|
287
|
+
needsTracedAsyncDataHelper = true;
|
|
310
288
|
modified = true;
|
|
311
289
|
} else {
|
|
312
290
|
const newCall = t.callExpression(t.identifier("__devFetchCall"), [
|
|
@@ -325,12 +303,12 @@ function fetchInstrumentPlugin() {
|
|
|
325
303
|
if (!modified) {
|
|
326
304
|
return null;
|
|
327
305
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const importStatement = importNames.length ? `import { ${importNames.join(", ")} } from 'nuxt-devtools-observatory/runtime/fetch-registry';
|
|
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';
|
|
333
310
|
` : "";
|
|
311
|
+
const importStatement = fetchImportStatement + asyncDataImportStatement;
|
|
334
312
|
const output = generate$1(ast, { retainLines: true }, scriptCode);
|
|
335
313
|
let finalCode;
|
|
336
314
|
if (isVue) {
|
|
@@ -554,6 +532,7 @@ const defaults = {
|
|
|
554
532
|
composableTracker: process.env.OBSERVATORY_COMPOSABLE_TRACKER === "true",
|
|
555
533
|
renderHeatmap: process.env.OBSERVATORY_RENDER_HEATMAP === "true",
|
|
556
534
|
transitionTracker: process.env.OBSERVATORY_TRANSITION_TRACKER === "true",
|
|
535
|
+
traceViewer: process.env.OBSERVATORY_TRACE_VIEWER === "true",
|
|
557
536
|
heatmapThresholdCount: process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3,
|
|
558
537
|
heatmapThresholdTime: process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600,
|
|
559
538
|
maxFetchEntries: process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200,
|
|
@@ -591,6 +570,7 @@ const module$1 = defineNuxtModule({
|
|
|
591
570
|
composableTracker: options.composableTracker ?? (process.env.OBSERVATORY_COMPOSABLE_TRACKER ? process.env.OBSERVATORY_COMPOSABLE_TRACKER === "true" : true),
|
|
592
571
|
renderHeatmap: options.renderHeatmap ?? (process.env.OBSERVATORY_RENDER_HEATMAP ? process.env.OBSERVATORY_RENDER_HEATMAP === "true" : true),
|
|
593
572
|
transitionTracker: options.transitionTracker ?? (process.env.OBSERVATORY_TRANSITION_TRACKER ? process.env.OBSERVATORY_TRANSITION_TRACKER === "true" : true),
|
|
573
|
+
traceViewer: options.traceViewer ?? (process.env.OBSERVATORY_TRACE_VIEWER ? process.env.OBSERVATORY_TRACE_VIEWER === "true" : true),
|
|
594
574
|
instrumentServer: options.instrumentServer ?? (process.env.OBSERVATORY_INSTRUMENT_SERVER ? process.env.OBSERVATORY_INSTRUMENT_SERVER === "true" : nuxt.options.ssr !== false),
|
|
595
575
|
heatmapThresholdCount: options.heatmapThresholdCount ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3),
|
|
596
576
|
heatmapThresholdTime: options.heatmapThresholdTime ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600),
|
|
@@ -611,6 +591,9 @@ const module$1 = defineNuxtModule({
|
|
|
611
591
|
"./runtime/composables/provide-inject-registry"
|
|
612
592
|
);
|
|
613
593
|
aliases["nuxt-devtools-observatory/runtime/fetch-registry"] = resolver.resolve("./runtime/composables/fetch-registry");
|
|
594
|
+
aliases["nuxt-devtools-observatory/runtime/async-data-instrumentation"] = resolver.resolve(
|
|
595
|
+
"./runtime/instrumentation/asyncData"
|
|
596
|
+
);
|
|
614
597
|
config.resolve = { ...config.resolve, alias: aliases };
|
|
615
598
|
});
|
|
616
599
|
const vitePluginScope = resolved.instrumentServer ? { server: true, client: true } : { server: false, client: true };
|
|
@@ -629,7 +612,7 @@ const module$1 = defineNuxtModule({
|
|
|
629
612
|
if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
|
|
630
613
|
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
631
614
|
}
|
|
632
|
-
if (resolved.fetchDashboard) {
|
|
615
|
+
if (resolved.fetchDashboard || resolved.traceViewer && resolved.instrumentServer) {
|
|
633
616
|
addServerPlugin(resolver.resolve("./runtime/nitro/fetch-capture"));
|
|
634
617
|
}
|
|
635
618
|
const base = "/__observatory";
|
|
@@ -645,13 +628,15 @@ const module$1 = defineNuxtModule({
|
|
|
645
628
|
composables: [],
|
|
646
629
|
renders: [],
|
|
647
630
|
transitions: [],
|
|
631
|
+
traces: [],
|
|
648
632
|
features: {
|
|
649
633
|
fetchDashboard: !!resolved.fetchDashboard,
|
|
650
634
|
provideInjectGraph: !!resolved.provideInjectGraph,
|
|
651
635
|
composableTracker: !!resolved.composableTracker,
|
|
652
636
|
composableNavigationMode: resolved.composableNavigationMode,
|
|
653
637
|
renderHeatmap: !!resolved.renderHeatmap,
|
|
654
|
-
transitionTracker: !!resolved.transitionTracker
|
|
638
|
+
transitionTracker: !!resolved.transitionTracker,
|
|
639
|
+
traceViewer: !!resolved.traceViewer
|
|
655
640
|
}
|
|
656
641
|
};
|
|
657
642
|
let rpc = null;
|
|
@@ -730,6 +715,7 @@ ${configScript}`);
|
|
|
730
715
|
composableTracker: resolved.composableTracker,
|
|
731
716
|
renderHeatmap: resolved.renderHeatmap,
|
|
732
717
|
transitionTracker: resolved.transitionTracker,
|
|
718
|
+
traceViewer: resolved.traceViewer,
|
|
733
719
|
maxFetchEntries: resolved.maxFetchEntries,
|
|
734
720
|
maxPayloadBytes: resolved.maxPayloadBytes,
|
|
735
721
|
maxTransitions: resolved.maxTransitions,
|
|
@@ -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) {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "#app";
|
|
2
|
+
import { traceStore } from "../tracing/traceStore.js";
|
|
2
3
|
export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
3
4
|
const entries = /* @__PURE__ */ new Map();
|
|
4
|
-
const pendingTriggeredRenders = /* @__PURE__ */ new Set();
|
|
5
|
-
const renderStartTimes = /* @__PURE__ */ new Map();
|
|
6
5
|
let currentRoute = "/";
|
|
7
6
|
const config = useRuntimeConfig().public.observatory;
|
|
8
7
|
const MAX_TIMELINE = config.maxRenderTimeline ?? 100;
|
|
9
8
|
const HIDE_INTERNALS = config.heatmapHideInternals ?? false;
|
|
10
9
|
let dirty = true;
|
|
11
10
|
let cachedSnapshot = "[]";
|
|
11
|
+
let resetTimestamp = 0;
|
|
12
|
+
const liveElements = /* @__PURE__ */ new Map();
|
|
12
13
|
function markDirty() {
|
|
13
14
|
dirty = true;
|
|
14
15
|
}
|
|
@@ -41,81 +42,27 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
41
42
|
}
|
|
42
43
|
return entries.get(uid);
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
markDirty();
|
|
48
|
-
}
|
|
49
|
-
function flushDirtyRects() {
|
|
50
|
-
if (dirtyRects.size === 0) {
|
|
45
|
+
function refreshEntryRect(uid) {
|
|
46
|
+
const entry = entries.get(uid);
|
|
47
|
+
if (!entry) {
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!entry) {
|
|
56
|
-
dirtyRects.delete(uid);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const el = _liveElements.get(uid);
|
|
60
|
-
if (!el) {
|
|
61
|
-
dirtyRects.delete(uid);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const rect = el.getBoundingClientRect?.();
|
|
65
|
-
entry.rect = rect ? {
|
|
66
|
-
x: Math.round(rect.x),
|
|
67
|
-
y: Math.round(rect.y),
|
|
68
|
-
width: Math.round(rect.width),
|
|
69
|
-
height: Math.round(rect.height),
|
|
70
|
-
top: Math.round(rect.top),
|
|
71
|
-
left: Math.round(rect.left)
|
|
72
|
-
} : void 0;
|
|
73
|
-
dirtyRects.delete(uid);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const _liveElements = /* @__PURE__ */ new Map();
|
|
77
|
-
function removeEntry(instance) {
|
|
78
|
-
const uid = instance.$.uid;
|
|
79
|
-
entries.delete(uid);
|
|
80
|
-
markDirty();
|
|
81
|
-
}
|
|
82
|
-
function startRenderTimer(uid) {
|
|
83
|
-
if (typeof performance === "undefined") {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
renderStartTimes.set(uid, performance.now());
|
|
87
|
-
}
|
|
88
|
-
function recordRenderDuration(entry, kind) {
|
|
89
|
-
if (typeof performance === "undefined") {
|
|
50
|
+
const el = liveElements.get(uid);
|
|
51
|
+
if (!el?.getBoundingClientRect) {
|
|
90
52
|
return;
|
|
91
53
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
entry.avgMs = Math.round(entry.totalMs / totalEvents * 10) / 10;
|
|
101
|
-
const lastTrigger = entry.triggers.length > 0 ? entry.triggers[entry.triggers.length - 1] : void 0;
|
|
102
|
-
const event = {
|
|
103
|
-
kind,
|
|
104
|
-
t: startedAt,
|
|
105
|
-
durationMs: Math.round(durationMs * 10) / 10,
|
|
106
|
-
triggerKey: kind === "update" && lastTrigger ? `${lastTrigger.type}: ${lastTrigger.key}` : void 0,
|
|
107
|
-
route: currentRoute
|
|
54
|
+
const rect = el.getBoundingClientRect();
|
|
55
|
+
entry.rect = {
|
|
56
|
+
x: Math.round(rect.x),
|
|
57
|
+
y: Math.round(rect.y),
|
|
58
|
+
width: Math.round(rect.width),
|
|
59
|
+
height: Math.round(rect.height),
|
|
60
|
+
top: Math.round(rect.top),
|
|
61
|
+
left: Math.round(rect.left)
|
|
108
62
|
};
|
|
109
|
-
entry.timeline.push(event);
|
|
110
|
-
if (entry.timeline.length > MAX_TIMELINE) {
|
|
111
|
-
entry.timeline.shift();
|
|
112
|
-
}
|
|
113
|
-
markDirty();
|
|
114
63
|
}
|
|
115
64
|
function reset() {
|
|
116
|
-
|
|
117
|
-
renderStartTimes.clear();
|
|
118
|
-
dirtyRects.clear();
|
|
65
|
+
resetTimestamp = performance.now();
|
|
119
66
|
for (const entry of entries.values()) {
|
|
120
67
|
entry.isPersistent = true;
|
|
121
68
|
entry.rerenders = 0;
|
|
@@ -126,65 +73,82 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
126
73
|
}
|
|
127
74
|
markDirty();
|
|
128
75
|
}
|
|
76
|
+
function aggregateFromComponentSpans() {
|
|
77
|
+
const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "component");
|
|
78
|
+
const allSpansByUid = /* @__PURE__ */ new Map();
|
|
79
|
+
const postResetSpansByUid = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const span of componentSpans) {
|
|
81
|
+
const uidValue = span.metadata?.uid;
|
|
82
|
+
const uid = typeof uidValue === "number" ? uidValue : Number(uidValue);
|
|
83
|
+
if (!Number.isFinite(uid)) {
|
|
84
|
+
continue;
|
|
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [uid, entry] of entries.entries()) {
|
|
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 lifecycle = span.metadata?.lifecycle === "mounted" ? "mount" : "update";
|
|
100
|
+
const routeValue = span.metadata?.route;
|
|
101
|
+
const route = typeof routeValue === "string" && routeValue.length > 0 ? routeValue : entry.route;
|
|
102
|
+
return {
|
|
103
|
+
kind: lifecycle,
|
|
104
|
+
t: span.startTime,
|
|
105
|
+
durationMs: Math.round((span.durationMs ?? 0) * 10) / 10,
|
|
106
|
+
route
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const mountCount = allSpans.filter((span) => span.metadata?.lifecycle === "mounted").length;
|
|
110
|
+
const rerenders = postResetSpans.filter((span) => span.metadata?.lifecycle !== "mounted").length;
|
|
111
|
+
const totalMs = postResetSpans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
|
|
112
|
+
const eventsCount = Math.max(postResetSpans.length, 1);
|
|
113
|
+
entry.mountCount = mountCount;
|
|
114
|
+
entry.rerenders = rerenders;
|
|
115
|
+
entry.totalMs = Math.round(totalMs * 10) / 10;
|
|
116
|
+
entry.avgMs = Math.round(totalMs / eventsCount * 10) / 10;
|
|
117
|
+
entry.timeline = timeline;
|
|
118
|
+
entry.triggers = [];
|
|
119
|
+
refreshEntryRect(uid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
129
122
|
nuxtApp.vueApp.mixin({
|
|
130
|
-
beforeMount() {
|
|
131
|
-
startRenderTimer(this.$.uid);
|
|
132
|
-
},
|
|
133
123
|
mounted() {
|
|
134
124
|
const entry = ensureEntry(this);
|
|
135
125
|
if (!entry) {
|
|
136
126
|
return;
|
|
137
127
|
}
|
|
138
|
-
entry.mountCount++;
|
|
139
128
|
const isHydration = options.isHydrating?.() ?? false;
|
|
140
|
-
if (isHydration && entry.mountCount ===
|
|
129
|
+
if (isHydration && entry.mountCount === 0) {
|
|
141
130
|
entry.isHydrationMount = true;
|
|
142
|
-
} else if (entry.mountCount > 1) {
|
|
143
|
-
entry.rerenders++;
|
|
144
|
-
}
|
|
145
|
-
_liveElements.set(entry.uid, this.$el);
|
|
146
|
-
markRectDirty(entry.uid);
|
|
147
|
-
if (!entry.isPersistent || entry.mountCount === 1) {
|
|
148
|
-
recordRenderDuration(entry, "mount");
|
|
149
|
-
} else {
|
|
150
|
-
renderStartTimes.delete(entry.uid);
|
|
151
131
|
}
|
|
132
|
+
liveElements.set(entry.uid, this.$el);
|
|
133
|
+
refreshEntryRect(entry.uid);
|
|
152
134
|
markDirty();
|
|
153
135
|
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
154
136
|
},
|
|
155
|
-
beforeUpdate() {
|
|
156
|
-
startRenderTimer(this.$.uid);
|
|
157
|
-
},
|
|
158
|
-
renderTriggered({ key, type }) {
|
|
159
|
-
const entry = ensureEntry(this);
|
|
160
|
-
if (!entry) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
|
|
164
|
-
pendingTriggeredRenders.add(entry.uid);
|
|
165
|
-
if (entry.triggers.length > 50) {
|
|
166
|
-
entry.triggers.shift();
|
|
167
|
-
}
|
|
168
|
-
markDirty();
|
|
169
|
-
},
|
|
170
137
|
updated() {
|
|
171
138
|
const entry = ensureEntry(this);
|
|
172
139
|
if (!entry) {
|
|
173
140
|
return;
|
|
174
141
|
}
|
|
175
|
-
|
|
176
|
-
entry.
|
|
177
|
-
markRectDirty(entry.uid);
|
|
178
|
-
recordRenderDuration(entry, "update");
|
|
142
|
+
liveElements.set(entry.uid, this.$el);
|
|
143
|
+
refreshEntryRect(entry.uid);
|
|
179
144
|
emit("render:update", { uid: entry.uid, renders: entry.rerenders });
|
|
145
|
+
markDirty();
|
|
180
146
|
},
|
|
181
147
|
unmounted() {
|
|
182
148
|
const uid = this.$.uid;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
_liveElements.delete(uid);
|
|
187
|
-
removeEntry(this);
|
|
149
|
+
liveElements.delete(uid);
|
|
150
|
+
entries.delete(uid);
|
|
151
|
+
markDirty();
|
|
188
152
|
emit("render:remove", { uid });
|
|
189
153
|
}
|
|
190
154
|
});
|
|
@@ -215,14 +179,14 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
215
179
|
};
|
|
216
180
|
}
|
|
217
181
|
function getAll() {
|
|
218
|
-
|
|
182
|
+
aggregateFromComponentSpans();
|
|
219
183
|
return [...entries.values()].map(sanitize);
|
|
220
184
|
}
|
|
221
185
|
function getSnapshot() {
|
|
222
186
|
if (!dirty) {
|
|
223
187
|
return cachedSnapshot;
|
|
224
188
|
}
|
|
225
|
-
|
|
189
|
+
aggregateFromComponentSpans();
|
|
226
190
|
try {
|
|
227
191
|
cachedSnapshot = JSON.stringify([...entries.values()].map(sanitize)) ?? "[]";
|
|
228
192
|
} catch {
|