nuxt-devtools-observatory 0.1.30 → 0.1.31

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 (39) hide show
  1. package/README.md +63 -4
  2. package/client/.env +2 -1
  3. package/client/.env.example +2 -1
  4. package/client/dist/assets/index-BuMXDBO9.js +17 -0
  5. package/client/dist/assets/{index-BmGW_M3W.css → index-CwcspZ6w.css} +1 -1
  6. package/client/dist/index.html +2 -2
  7. package/client/src/App.vue +4 -0
  8. package/client/src/components/Flamegraph.vue +443 -0
  9. package/client/src/components/SpanInspector.vue +446 -0
  10. package/client/src/components/TraceFilter.vue +344 -0
  11. package/client/src/components/WaterfallView.vue +443 -0
  12. package/client/src/composables/useTraceFilter.ts +164 -0
  13. package/client/src/stores/observatory.ts +5 -1
  14. package/client/src/views/TraceViewer.vue +599 -0
  15. package/client/src/views/TransitionTimeline.vue +1 -6
  16. package/dist/module.d.mts +5 -0
  17. package/dist/module.json +1 -1
  18. package/dist/module.mjs +30 -44
  19. package/dist/runtime/composables/render-registry.js +66 -110
  20. package/dist/runtime/composables/transition-registry.js +103 -28
  21. package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
  22. package/dist/runtime/instrumentation/asyncData.js +49 -0
  23. package/dist/runtime/instrumentation/component.d.ts +2 -0
  24. package/dist/runtime/instrumentation/component.js +126 -0
  25. package/dist/runtime/instrumentation/fetch.d.ts +2 -0
  26. package/dist/runtime/instrumentation/fetch.js +89 -0
  27. package/dist/runtime/instrumentation/route.d.ts +6 -0
  28. package/dist/runtime/instrumentation/route.js +41 -0
  29. package/dist/runtime/plugin.js +38 -2
  30. package/dist/runtime/tracing/context.d.ts +9 -0
  31. package/dist/runtime/tracing/context.js +15 -0
  32. package/dist/runtime/tracing/trace.d.ts +25 -0
  33. package/dist/runtime/tracing/trace.js +0 -0
  34. package/dist/runtime/tracing/traceStore.d.ts +39 -0
  35. package/dist/runtime/tracing/traceStore.js +101 -0
  36. package/dist/runtime/tracing/tracing.d.ts +27 -0
  37. package/dist/runtime/tracing/tracing.js +48 -0
  38. package/package.json +1 -1
  39. package/client/dist/assets/index-BCaKoHBH.js +0 -17
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';
333
308
  ` : "";
309
+ const asyncDataImportStatement = needsTracedAsyncDataHelper && !hasTracedAsyncDataImport ? `import { useTracedAsyncData } from 'nuxt-devtools-observatory/runtime/async-data-instrumentation';
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 };
@@ -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,
@@ -1,14 +1,14 @@
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
+ const liveElements = /* @__PURE__ */ new Map();
12
12
  function markDirty() {
13
13
  dirty = true;
14
14
  }
@@ -41,81 +41,26 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
41
41
  }
42
42
  return entries.get(uid);
43
43
  }
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) {
44
+ function refreshEntryRect(uid) {
45
+ const entry = entries.get(uid);
46
+ if (!entry) {
51
47
  return;
52
48
  }
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") {
49
+ const el = liveElements.get(uid);
50
+ if (!el?.getBoundingClientRect) {
90
51
  return;
91
52
  }
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
53
+ const rect = el.getBoundingClientRect();
54
+ entry.rect = {
55
+ x: Math.round(rect.x),
56
+ y: Math.round(rect.y),
57
+ width: Math.round(rect.width),
58
+ height: Math.round(rect.height),
59
+ top: Math.round(rect.top),
60
+ left: Math.round(rect.left)
108
61
  };
109
- entry.timeline.push(event);
110
- if (entry.timeline.length > MAX_TIMELINE) {
111
- entry.timeline.shift();
112
- }
113
- markDirty();
114
62
  }
115
63
  function reset() {
116
- pendingTriggeredRenders.clear();
117
- renderStartTimes.clear();
118
- dirtyRects.clear();
119
64
  for (const entry of entries.values()) {
120
65
  entry.isPersistent = true;
121
66
  entry.rerenders = 0;
@@ -126,65 +71,76 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
126
71
  }
127
72
  markDirty();
128
73
  }
74
+ function aggregateFromComponentSpans() {
75
+ const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "component");
76
+ const spansByUid = /* @__PURE__ */ new Map();
77
+ for (const span of componentSpans) {
78
+ const uidValue = span.metadata?.uid;
79
+ const uid = typeof uidValue === "number" ? uidValue : Number(uidValue);
80
+ if (!Number.isFinite(uid)) {
81
+ continue;
82
+ }
83
+ const list = spansByUid.get(uid) ?? [];
84
+ list.push(span);
85
+ spansByUid.set(uid, list);
86
+ }
87
+ for (const [uid, entry] of entries.entries()) {
88
+ const spans = spansByUid.get(uid) ?? [];
89
+ spans.sort((a, b) => a.startTime - b.startTime);
90
+ const timeline = spans.slice(-MAX_TIMELINE).map((span) => {
91
+ const lifecycle = span.metadata?.lifecycle === "mounted" ? "mount" : "update";
92
+ const routeValue = span.metadata?.route;
93
+ const route = typeof routeValue === "string" && routeValue.length > 0 ? routeValue : entry.route;
94
+ return {
95
+ kind: lifecycle,
96
+ t: span.startTime,
97
+ durationMs: Math.round((span.durationMs ?? 0) * 10) / 10,
98
+ route
99
+ };
100
+ });
101
+ const mountCount = spans.filter((span) => span.metadata?.lifecycle === "mounted").length;
102
+ const rerenders = spans.filter((span) => span.metadata?.lifecycle !== "mounted").length;
103
+ const totalMs = spans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
104
+ const eventsCount = Math.max(mountCount + rerenders, 1);
105
+ entry.mountCount = mountCount;
106
+ entry.rerenders = rerenders;
107
+ entry.totalMs = Math.round(totalMs * 10) / 10;
108
+ entry.avgMs = Math.round(totalMs / eventsCount * 10) / 10;
109
+ entry.timeline = timeline;
110
+ entry.triggers = [];
111
+ refreshEntryRect(uid);
112
+ }
113
+ }
129
114
  nuxtApp.vueApp.mixin({
130
- beforeMount() {
131
- startRenderTimer(this.$.uid);
132
- },
133
115
  mounted() {
134
116
  const entry = ensureEntry(this);
135
117
  if (!entry) {
136
118
  return;
137
119
  }
138
- entry.mountCount++;
139
120
  const isHydration = options.isHydrating?.() ?? false;
140
- if (isHydration && entry.mountCount === 1) {
121
+ if (isHydration && entry.mountCount === 0) {
141
122
  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
123
  }
124
+ liveElements.set(entry.uid, this.$el);
125
+ refreshEntryRect(entry.uid);
152
126
  markDirty();
153
127
  emit("render:update", { uid: entry.uid, renders: entry.rerenders });
154
128
  },
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
129
  updated() {
171
130
  const entry = ensureEntry(this);
172
131
  if (!entry) {
173
132
  return;
174
133
  }
175
- pendingTriggeredRenders.delete(entry.uid);
176
- entry.rerenders++;
177
- markRectDirty(entry.uid);
178
- recordRenderDuration(entry, "update");
134
+ liveElements.set(entry.uid, this.$el);
135
+ refreshEntryRect(entry.uid);
179
136
  emit("render:update", { uid: entry.uid, renders: entry.rerenders });
137
+ markDirty();
180
138
  },
181
139
  unmounted() {
182
140
  const uid = this.$.uid;
183
- pendingTriggeredRenders.delete(uid);
184
- renderStartTimes.delete(uid);
185
- dirtyRects.delete(uid);
186
- _liveElements.delete(uid);
187
- removeEntry(this);
141
+ liveElements.delete(uid);
142
+ entries.delete(uid);
143
+ markDirty();
188
144
  emit("render:remove", { uid });
189
145
  }
190
146
  });
@@ -215,14 +171,14 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
215
171
  };
216
172
  }
217
173
  function getAll() {
218
- flushDirtyRects();
174
+ aggregateFromComponentSpans();
219
175
  return [...entries.values()].map(sanitize);
220
176
  }
221
177
  function getSnapshot() {
222
178
  if (!dirty) {
223
179
  return cachedSnapshot;
224
180
  }
225
- flushDirtyRects();
181
+ aggregateFromComponentSpans();
226
182
  try {
227
183
  cachedSnapshot = JSON.stringify([...entries.values()].map(sanitize)) ?? "[]";
228
184
  } catch {
@@ -1,65 +1,140 @@
1
1
  import { h, defineComponent, getCurrentInstance, onUnmounted, Transition as VueTransition } from "vue";
2
+ import { startSpan } from "../tracing/tracing.js";
3
+ import { traceStore } from "../tracing/traceStore.js";
2
4
  const MAX_TRANSITIONS = typeof process !== "undefined" && process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500;
3
5
  export function setupTransitionRegistry() {
4
- const entries = /* @__PURE__ */ new Map();
6
+ const activeSpans = /* @__PURE__ */ new Map();
7
+ const entryState = /* @__PURE__ */ new Map();
5
8
  let dirty = true;
6
9
  let cachedSnapshot = "[]";
7
10
  function markDirty() {
8
11
  dirty = true;
9
12
  }
10
13
  function register(entry) {
11
- if (entries.size >= MAX_TRANSITIONS) {
12
- const oldestKey = entries.keys().next().value;
13
- if (oldestKey !== void 0) {
14
- entries.delete(oldestKey);
15
- }
16
- }
17
- entries.set(entry.id, entry);
14
+ const spanHandle = startSpan({
15
+ name: `transition:${entry.transitionName}`,
16
+ type: "transition",
17
+ metadata: {
18
+ id: entry.id,
19
+ transitionName: entry.transitionName,
20
+ parentComponent: entry.parentComponent,
21
+ direction: entry.direction,
22
+ phase: entry.phase,
23
+ cancelled: entry.cancelled,
24
+ appear: entry.appear,
25
+ mode: entry.mode
26
+ },
27
+ startTime: entry.startTime
28
+ });
29
+ activeSpans.set(entry.id, spanHandle);
30
+ entryState.set(entry.id, {
31
+ id: entry.id,
32
+ transitionName: entry.transitionName,
33
+ parentComponent: entry.parentComponent,
34
+ direction: entry.direction,
35
+ phase: entry.phase,
36
+ startTime: entry.startTime,
37
+ endTime: entry.endTime,
38
+ cancelled: entry.cancelled,
39
+ appear: entry.appear,
40
+ mode: entry.mode
41
+ });
18
42
  markDirty();
19
43
  emit("transition:register", entry);
20
44
  }
21
45
  function clear() {
22
- entries.clear();
46
+ activeSpans.clear();
47
+ entryState.clear();
23
48
  markDirty();
24
49
  emit("transition:clear", {});
25
50
  }
26
51
  function update(id, patch) {
27
- const existing = entries.get(id);
52
+ const existing = entryState.get(id);
28
53
  if (!existing) {
29
54
  return;
30
55
  }
31
56
  const updated = { ...existing, ...patch };
32
- if (patch.endTime !== void 0) {
33
- updated.durationMs = Math.round((patch.endTime - existing.startTime) * 10) / 10;
57
+ entryState.set(id, updated);
58
+ const active = activeSpans.get(id);
59
+ if (active) {
60
+ active.span.metadata = {
61
+ ...active.span.metadata ?? {},
62
+ id: updated.id,
63
+ transitionName: updated.transitionName,
64
+ parentComponent: updated.parentComponent,
65
+ direction: updated.direction,
66
+ phase: updated.phase,
67
+ cancelled: updated.cancelled,
68
+ appear: updated.appear,
69
+ mode: updated.mode
70
+ };
71
+ if (patch.endTime !== void 0) {
72
+ active.end({
73
+ endTime: patch.endTime,
74
+ status: patch.cancelled === true ? "cancelled" : "ok",
75
+ metadata: {
76
+ phase: updated.phase,
77
+ cancelled: updated.cancelled
78
+ }
79
+ });
80
+ activeSpans.delete(id);
81
+ }
34
82
  }
35
- entries.set(id, updated);
36
83
  markDirty();
37
- emit("transition:update", updated);
84
+ emit("transition:update", toTransitionEntry(updated, active?.span));
38
85
  }
39
- function sanitize(entry) {
86
+ function toTransitionEntry(base, span) {
87
+ const durationMs = span?.durationMs ?? (base.endTime !== void 0 ? Math.round((base.endTime - base.startTime) * 10) / 10 : void 0);
40
88
  return {
41
- id: entry.id,
42
- transitionName: entry.transitionName,
43
- parentComponent: entry.parentComponent,
44
- direction: entry.direction,
45
- phase: entry.phase,
46
- startTime: entry.startTime,
47
- endTime: entry.endTime,
48
- durationMs: entry.durationMs,
49
- cancelled: entry.cancelled,
50
- appear: entry.appear,
51
- mode: entry.mode
89
+ id: base.id,
90
+ transitionName: base.transitionName,
91
+ parentComponent: base.parentComponent,
92
+ direction: base.direction,
93
+ phase: base.phase,
94
+ startTime: base.startTime,
95
+ endTime: base.endTime,
96
+ durationMs,
97
+ cancelled: base.cancelled,
98
+ appear: base.appear,
99
+ mode: base.mode
52
100
  };
53
101
  }
54
102
  function getAll() {
55
- return [...entries.values()].map(sanitize);
103
+ const spans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "transition").sort((a, b) => a.startTime - b.startTime);
104
+ const entries = spans.map((span) => {
105
+ const metadata = span.metadata ?? {};
106
+ const id = typeof metadata.id === "string" ? metadata.id : span.id;
107
+ const transitionName = typeof metadata.transitionName === "string" ? metadata.transitionName : "default";
108
+ const parentComponent = typeof metadata.parentComponent === "string" ? metadata.parentComponent : "unknown";
109
+ const direction = metadata.direction === "leave" ? "leave" : "enter";
110
+ const knownPhase = metadata.phase;
111
+ const phase = knownPhase === "entering" || knownPhase === "entered" || knownPhase === "leaving" || knownPhase === "left" || knownPhase === "enter-cancelled" || knownPhase === "leave-cancelled" || knownPhase === "interrupted" ? knownPhase : span.endTime ? direction === "enter" ? "entered" : "left" : direction === "enter" ? "entering" : "leaving";
112
+ return {
113
+ id,
114
+ transitionName,
115
+ parentComponent,
116
+ direction,
117
+ phase,
118
+ startTime: span.startTime,
119
+ endTime: span.endTime,
120
+ durationMs: span.durationMs,
121
+ cancelled: metadata.cancelled === true || span.status === "cancelled",
122
+ appear: metadata.appear === true,
123
+ mode: typeof metadata.mode === "string" ? metadata.mode : void 0
124
+ };
125
+ });
126
+ const overflow = entries.length - MAX_TRANSITIONS;
127
+ if (overflow > 0) {
128
+ return entries.slice(overflow);
129
+ }
130
+ return entries;
56
131
  }
57
132
  function getSnapshot() {
58
133
  if (!dirty) {
59
134
  return cachedSnapshot;
60
135
  }
61
136
  try {
62
- cachedSnapshot = JSON.stringify([...entries.values()].map(sanitize)) ?? "[]";
137
+ cachedSnapshot = JSON.stringify(getAll()) ?? "[]";
63
138
  } catch {
64
139
  cachedSnapshot = "[]";
65
140
  }
@@ -0,0 +1,9 @@
1
+ interface AsyncDataMeta {
2
+ key: string;
3
+ file: string;
4
+ line: number;
5
+ originalFn?: string;
6
+ }
7
+ type AnyFn = (...args: any[]) => any;
8
+ export declare function useTracedAsyncData<TFn extends AnyFn>(originalFn: TFn, args: unknown[], handlerIndex: number, key: unknown, meta: AsyncDataMeta): ReturnType<TFn>;
9
+ export {};
@@ -0,0 +1,49 @@
1
+ import { startSpan } from "../tracing/tracing.js";
2
+ function getNormalizedKey(key) {
3
+ return typeof key === "string" && key.length > 0 ? key : "useAsyncData";
4
+ }
5
+ export function useTracedAsyncData(originalFn, args, handlerIndex, key, meta) {
6
+ if (!import.meta.dev || !import.meta.client) {
7
+ return originalFn(...args);
8
+ }
9
+ const nextArgs = [...args];
10
+ const originalHandler = nextArgs[handlerIndex];
11
+ if (typeof originalHandler !== "function") {
12
+ return originalFn(...args);
13
+ }
14
+ const normalizedKey = getNormalizedKey(key);
15
+ nextArgs[handlerIndex] = (...handlerArgs) => {
16
+ const span = startSpan({
17
+ name: meta.originalFn ?? "useAsyncData",
18
+ type: "fetch",
19
+ metadata: {
20
+ key: normalizedKey,
21
+ status: "pending",
22
+ file: meta.file,
23
+ line: meta.line,
24
+ source: "async-data"
25
+ }
26
+ });
27
+ return Promise.resolve(originalHandler(...handlerArgs)).then((result) => {
28
+ span.end({
29
+ status: "ok",
30
+ metadata: {
31
+ key: normalizedKey,
32
+ status: "ok"
33
+ }
34
+ });
35
+ return result;
36
+ }).catch((error) => {
37
+ span.end({
38
+ status: "error",
39
+ metadata: {
40
+ key: normalizedKey,
41
+ status: "error",
42
+ errorMessage: error instanceof Error ? error.message : String(error)
43
+ }
44
+ });
45
+ throw error;
46
+ });
47
+ };
48
+ return originalFn(...nextArgs);
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { NuxtApp } from '#app';
2
+ export declare function setupComponentInstrumentation(nuxtApp: NuxtApp): void;