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.
- package/README.md +93 -11
- package/client/.env +2 -1
- package/client/.env.example +2 -1
- package/client/dist/assets/index-BuMXDBO9.js +17 -0
- package/client/dist/assets/index-CwcspZ6w.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +443 -0
- package/client/src/components/SpanInspector.vue +446 -0
- package/client/src/components/TraceFilter.vue +344 -0
- package/client/src/components/WaterfallView.vue +443 -0
- package/client/src/composables/useResizablePane.ts +65 -0
- package/client/src/composables/useTraceFilter.ts +164 -0
- package/client/src/stores/observatory.ts +16 -2
- package/client/src/style.css +203 -28
- package/client/src/views/ComposableTracker.vue +324 -259
- package/client/src/views/FetchDashboard.vue +104 -133
- package/client/src/views/ProvideInjectGraph.vue +99 -109
- package/client/src/views/RenderHeatmap.vue +191 -147
- package/client/src/views/TraceViewer.vue +599 -0
- package/client/src/views/TransitionTimeline.vue +167 -137
- package/client/tsconfig.json +3 -1
- package/client/vite.config.ts +8 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +186 -200
- package/dist/runtime/composables/render-registry.js +66 -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/plugin.js +39 -3
- 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 +9 -6
- package/client/dist/assets/index-DXCGQOSF.js +0 -17
- 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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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") {
|
|
49
|
+
const el = liveElements.get(uid);
|
|
50
|
+
if (!el?.getBoundingClientRect) {
|
|
90
51
|
return;
|
|
91
52
|
}
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
176
|
-
entry.
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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 =
|
|
52
|
+
const existing = entryState.get(id);
|
|
28
53
|
if (!existing) {
|
|
29
54
|
return;
|
|
30
55
|
}
|
|
31
56
|
const updated = { ...existing, ...patch };
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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:
|
|
42
|
-
transitionName:
|
|
43
|
-
parentComponent:
|
|
44
|
-
direction:
|
|
45
|
-
phase:
|
|
46
|
-
startTime:
|
|
47
|
-
endTime:
|
|
48
|
-
durationMs
|
|
49
|
-
cancelled:
|
|
50
|
-
appear:
|
|
51
|
-
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
|
-
|
|
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(
|
|
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,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,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
|
+
}
|