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.
Files changed (51) hide show
  1. package/README.md +117 -30
  2. package/client/.env.example +2 -1
  3. package/client/dist/assets/index-5Wl1XYRH.js +17 -0
  4. package/client/dist/assets/index-DT_QUiIh.css +1 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +4 -0
  7. package/client/src/components/Flamegraph.vue +442 -0
  8. package/client/src/components/SpanInspector.vue +446 -0
  9. package/client/src/components/TraceFilter.vue +342 -0
  10. package/client/src/components/WaterfallView.vue +443 -0
  11. package/client/src/composables/composable-search.ts +124 -0
  12. package/client/src/composables/trace-render-aggregation.ts +254 -0
  13. package/client/src/composables/useExportImport.ts +63 -0
  14. package/client/src/composables/useTraceFilter.ts +160 -0
  15. package/client/src/stores/observatory.ts +13 -1
  16. package/client/src/views/ComposableTracker.vue +65 -30
  17. package/client/src/views/RenderHeatmap.vue +63 -1
  18. package/client/src/views/TraceViewer.vue +1212 -0
  19. package/client/src/views/TransitionTimeline.vue +1 -6
  20. package/dist/module.d.mts +5 -0
  21. package/dist/module.json +1 -1
  22. package/dist/module.mjs +31 -45
  23. package/dist/runtime/composables/composable-registry.d.ts +19 -0
  24. package/dist/runtime/composables/composable-registry.js +63 -5
  25. package/dist/runtime/composables/render-registry.js +74 -110
  26. package/dist/runtime/composables/transition-registry.js +103 -28
  27. package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
  28. package/dist/runtime/instrumentation/asyncData.js +49 -0
  29. package/dist/runtime/instrumentation/component.d.ts +2 -0
  30. package/dist/runtime/instrumentation/component.js +126 -0
  31. package/dist/runtime/instrumentation/fetch.d.ts +2 -0
  32. package/dist/runtime/instrumentation/fetch.js +89 -0
  33. package/dist/runtime/instrumentation/route.d.ts +6 -0
  34. package/dist/runtime/instrumentation/route.js +41 -0
  35. package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
  36. package/dist/runtime/nitro/fetch-capture.js +85 -7
  37. package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
  38. package/dist/runtime/nitro/ssr-trace-store.js +84 -0
  39. package/dist/runtime/plugin.js +82 -2
  40. package/dist/runtime/tracing/context.d.ts +9 -0
  41. package/dist/runtime/tracing/context.js +15 -0
  42. package/dist/runtime/tracing/trace.d.ts +25 -0
  43. package/dist/runtime/tracing/trace.js +0 -0
  44. package/dist/runtime/tracing/traceStore.d.ts +39 -0
  45. package/dist/runtime/tracing/traceStore.js +101 -0
  46. package/dist/runtime/tracing/tracing.d.ts +27 -0
  47. package/dist/runtime/tracing/tracing.js +48 -0
  48. package/package.json +5 -1
  49. package/client/.env +0 -16
  50. package/client/dist/assets/index-BCaKoHBH.js +0 -17
  51. 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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.30",
7
+ "version": "0.1.32",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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 needsFetchHandlerHelper = false;
213
+ let needsTracedAsyncDataHelper = false;
214
214
  const hasFetchCallImport = scriptCode.includes("__devFetchCall");
215
- const hasFetchHandlerImport = scriptCode.includes("__devFetchHandler");
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", "__devFetchHandler"].includes(path.parent.callee.name)) {
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 wrappedHandler = t.arrowFunctionExpression(
277
- [t.restElement(t.identifier("args"))],
278
- t.conditionalExpression(
279
- t.logicalExpression(
280
- "&&",
281
- t.memberExpression(t.identifier("process"), t.identifier("dev")),
282
- t.memberExpression(t.identifier("process"), t.identifier("client"))
283
- ),
284
- t.callExpression(
285
- t.callExpression(t.identifier("__devFetchHandler"), [
286
- handlerArg,
287
- keyArg ?? t.stringLiteral(key),
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 importNames = [
329
- needsFetchCallHelper && !hasFetchCallImport ? "__devFetchCall" : "",
330
- needsFetchHandlerHelper && !hasFetchHandlerImport ? "__devFetchHandler" : ""
331
- ].filter(Boolean);
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: getSharedKeys(entry.id, entry.name),
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
- return callFn();
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
- const dirtyRects = /* @__PURE__ */ new Set();
45
- function markRectDirty(uid) {
46
- dirtyRects.add(uid);
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
- for (const uid of dirtyRects) {
54
- const entry = entries.get(uid);
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 startedAt = renderStartTimes.get(entry.uid);
93
- if (startedAt === void 0) {
94
- return;
95
- }
96
- renderStartTimes.delete(entry.uid);
97
- const durationMs = Math.max(performance.now() - startedAt, 0);
98
- entry.totalMs += durationMs;
99
- const totalEvents = (entry.rerenders ?? 0) + Math.max(entry.mountCount, 1);
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
- pendingTriggeredRenders.clear();
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 === 1) {
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
- pendingTriggeredRenders.delete(entry.uid);
176
- entry.rerenders++;
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
- pendingTriggeredRenders.delete(uid);
184
- renderStartTimes.delete(uid);
185
- dirtyRects.delete(uid);
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
- flushDirtyRects();
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
- flushDirtyRects();
189
+ aggregateFromComponentSpans();
226
190
  try {
227
191
  cachedSnapshot = JSON.stringify([...entries.values()].map(sanitize)) ?? "[]";
228
192
  } catch {