nuxt-devtools-observatory 0.1.28 → 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 (48) hide show
  1. package/README.md +93 -11
  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-CwcspZ6w.css +1 -0
  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/useResizablePane.ts +65 -0
  13. package/client/src/composables/useTraceFilter.ts +164 -0
  14. package/client/src/stores/observatory.ts +16 -2
  15. package/client/src/style.css +203 -28
  16. package/client/src/views/ComposableTracker.vue +324 -259
  17. package/client/src/views/FetchDashboard.vue +104 -133
  18. package/client/src/views/ProvideInjectGraph.vue +99 -109
  19. package/client/src/views/RenderHeatmap.vue +191 -147
  20. package/client/src/views/TraceViewer.vue +599 -0
  21. package/client/src/views/TransitionTimeline.vue +167 -137
  22. package/client/tsconfig.json +3 -1
  23. package/client/vite.config.ts +8 -0
  24. package/dist/module.d.mts +5 -0
  25. package/dist/module.json +1 -1
  26. package/dist/module.mjs +186 -200
  27. package/dist/runtime/composables/render-registry.js +66 -110
  28. package/dist/runtime/composables/transition-registry.js +103 -28
  29. package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
  30. package/dist/runtime/instrumentation/asyncData.js +49 -0
  31. package/dist/runtime/instrumentation/component.d.ts +2 -0
  32. package/dist/runtime/instrumentation/component.js +126 -0
  33. package/dist/runtime/instrumentation/fetch.d.ts +2 -0
  34. package/dist/runtime/instrumentation/fetch.js +89 -0
  35. package/dist/runtime/instrumentation/route.d.ts +6 -0
  36. package/dist/runtime/instrumentation/route.js +41 -0
  37. package/dist/runtime/plugin.js +39 -3
  38. package/dist/runtime/tracing/context.d.ts +9 -0
  39. package/dist/runtime/tracing/context.js +15 -0
  40. package/dist/runtime/tracing/trace.d.ts +25 -0
  41. package/dist/runtime/tracing/trace.js +0 -0
  42. package/dist/runtime/tracing/traceStore.d.ts +39 -0
  43. package/dist/runtime/tracing/traceStore.js +101 -0
  44. package/dist/runtime/tracing/tracing.d.ts +27 -0
  45. package/dist/runtime/tracing/tracing.js +48 -0
  46. package/package.json +9 -6
  47. package/client/dist/assets/index-DXCGQOSF.js +0 -17
  48. package/client/dist/assets/index-htI4WwBU.css +0 -1
@@ -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;
@@ -0,0 +1,126 @@
1
+ import { startSpan } from "../tracing/tracing.js";
2
+ const instrumentedApps = /* @__PURE__ */ new WeakSet();
3
+ const renderStartTimes = /* @__PURE__ */ new WeakMap();
4
+ function getComponentFile(instance) {
5
+ return instance.$.type.__file;
6
+ }
7
+ function getComponentName(instance) {
8
+ const type = instance.$.type;
9
+ if (type.__name && type.__name.length > 0) {
10
+ return type.__name;
11
+ }
12
+ if (type.name && type.name.length > 0) {
13
+ return type.name;
14
+ }
15
+ if (type.__file) {
16
+ const fileName = type.__file.split("/").pop() ?? type.__file;
17
+ return fileName.replace(/\.[^.]+$/, "");
18
+ }
19
+ return "AnonymousComponent";
20
+ }
21
+ function shouldTrack(instance) {
22
+ const file = getComponentFile(instance);
23
+ if (!file) {
24
+ return false;
25
+ }
26
+ return !file.includes("node_modules");
27
+ }
28
+ function trackLifecycle(instance, lifecycle) {
29
+ if (!shouldTrack(instance)) {
30
+ return;
31
+ }
32
+ const componentName = getComponentName(instance);
33
+ const file = getComponentFile(instance);
34
+ const uid = instance.$.uid;
35
+ const route = typeof window !== "undefined" ? window.location.pathname : "/";
36
+ const span = startSpan({
37
+ name: `component:${lifecycle}`,
38
+ type: "component",
39
+ metadata: {
40
+ lifecycle,
41
+ componentName,
42
+ uid,
43
+ file,
44
+ route,
45
+ status: "ok"
46
+ }
47
+ });
48
+ span.end({
49
+ status: "ok",
50
+ metadata: {
51
+ lifecycle,
52
+ componentName,
53
+ uid,
54
+ file,
55
+ route,
56
+ status: "ok"
57
+ }
58
+ });
59
+ }
60
+ function trackRender(instance, phase, startTime, endTime) {
61
+ if (!shouldTrack(instance)) {
62
+ return;
63
+ }
64
+ const componentName = getComponentName(instance);
65
+ const file = getComponentFile(instance);
66
+ const uid = instance.$.uid;
67
+ const route = typeof window !== "undefined" ? window.location.pathname : "/";
68
+ const span = startSpan({
69
+ name: "component:render",
70
+ type: "render",
71
+ startTime,
72
+ metadata: {
73
+ lifecycle: `render:${phase}`,
74
+ componentName,
75
+ uid,
76
+ file,
77
+ route
78
+ }
79
+ });
80
+ span.end({
81
+ endTime,
82
+ status: "ok",
83
+ metadata: {
84
+ lifecycle: `render:${phase}`,
85
+ componentName,
86
+ uid,
87
+ file,
88
+ route
89
+ }
90
+ });
91
+ }
92
+ export function setupComponentInstrumentation(nuxtApp) {
93
+ const app = nuxtApp.vueApp;
94
+ if (instrumentedApps.has(app)) {
95
+ return;
96
+ }
97
+ instrumentedApps.add(app);
98
+ app.mixin({
99
+ beforeMount() {
100
+ if (shouldTrack(this)) {
101
+ renderStartTimes.set(this, performance.now());
102
+ }
103
+ },
104
+ mounted() {
105
+ const startTime = renderStartTimes.get(this);
106
+ renderStartTimes.delete(this);
107
+ if (startTime !== void 0) {
108
+ trackRender(this, "mount", startTime, performance.now());
109
+ }
110
+ trackLifecycle(this, "mounted");
111
+ },
112
+ beforeUpdate() {
113
+ if (shouldTrack(this)) {
114
+ renderStartTimes.set(this, performance.now());
115
+ }
116
+ },
117
+ updated() {
118
+ const startTime = renderStartTimes.get(this);
119
+ renderStartTimes.delete(this);
120
+ if (startTime !== void 0) {
121
+ trackRender(this, "update", startTime, performance.now());
122
+ }
123
+ trackLifecycle(this, "updated");
124
+ }
125
+ });
126
+ }
@@ -0,0 +1,2 @@
1
+ import type { NuxtApp } from '#app';
2
+ export declare function setupFetchInstrumentation(nuxtApp: NuxtApp): void;
@@ -0,0 +1,89 @@
1
+ import { startSpan } from "../tracing/tracing.js";
2
+ function resolveUrl(input) {
3
+ if (typeof input === "string") {
4
+ return input;
5
+ }
6
+ if (input && typeof input === "object" && "url" in input) {
7
+ const maybeUrl = input.url;
8
+ return typeof maybeUrl === "string" ? maybeUrl : String(maybeUrl ?? "");
9
+ }
10
+ return String(input ?? "");
11
+ }
12
+ function resolveMethod(input, options) {
13
+ const method = options?.method;
14
+ if (typeof method === "string" && method.length > 0) {
15
+ return method.toUpperCase();
16
+ }
17
+ if (input && typeof input === "object" && "method" in input) {
18
+ const requestMethod = input.method;
19
+ if (typeof requestMethod === "string" && requestMethod.length > 0) {
20
+ return requestMethod.toUpperCase();
21
+ }
22
+ }
23
+ return "GET";
24
+ }
25
+ function resolveErrorStatus(error) {
26
+ const target = error;
27
+ return target?.response?.status ?? target?.statusCode ?? target?.status;
28
+ }
29
+ const WRAPPED_FETCH_FLAG = "__observatory_wrapped_fetch__";
30
+ export function setupFetchInstrumentation(nuxtApp) {
31
+ const original = nuxtApp.$fetch;
32
+ if (!original) {
33
+ return;
34
+ }
35
+ if (original[WRAPPED_FETCH_FLAG]) {
36
+ return;
37
+ }
38
+ const wrapped = ((request, options) => {
39
+ const url = resolveUrl(request);
40
+ const method = resolveMethod(request, options);
41
+ const startedAt = performance.now();
42
+ const span = startSpan({
43
+ name: "$fetch",
44
+ type: "fetch",
45
+ metadata: {
46
+ source: "$fetch",
47
+ url,
48
+ method,
49
+ status: "pending"
50
+ }
51
+ });
52
+ return Promise.resolve(original(request, options)).then((result) => {
53
+ const durationMs = Math.max(performance.now() - startedAt, 0);
54
+ span.end({
55
+ status: "ok",
56
+ metadata: {
57
+ source: "$fetch",
58
+ url,
59
+ method,
60
+ status: "ok",
61
+ durationMs: Math.round(durationMs * 10) / 10
62
+ }
63
+ });
64
+ return result;
65
+ }).catch((error) => {
66
+ const durationMs = Math.max(performance.now() - startedAt, 0);
67
+ const statusCode = resolveErrorStatus(error);
68
+ span.end({
69
+ status: "error",
70
+ metadata: {
71
+ source: "$fetch",
72
+ url,
73
+ method,
74
+ status: "error",
75
+ statusCode,
76
+ durationMs: Math.round(durationMs * 10) / 10
77
+ }
78
+ });
79
+ throw error;
80
+ });
81
+ });
82
+ Object.assign(wrapped, original);
83
+ wrapped[WRAPPED_FETCH_FLAG] = true;
84
+ nuxtApp.$fetch = wrapped;
85
+ const globalTarget = globalThis;
86
+ if (typeof globalTarget.$fetch === "function") {
87
+ globalTarget.$fetch = wrapped;
88
+ }
89
+ }