nuxt-devtools-observatory 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -30
- package/client/.env.example +2 -1
- package/client/dist/assets/index-5Wl1XYRH.js +17 -0
- package/client/dist/assets/index-DT_QUiIh.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +442 -0
- package/client/src/components/SpanInspector.vue +446 -0
- package/client/src/components/TraceFilter.vue +342 -0
- package/client/src/components/WaterfallView.vue +443 -0
- package/client/src/composables/composable-search.ts +124 -0
- package/client/src/composables/trace-render-aggregation.ts +254 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +160 -0
- package/client/src/stores/observatory.ts +13 -1
- package/client/src/views/ComposableTracker.vue +65 -30
- package/client/src/views/RenderHeatmap.vue +63 -1
- package/client/src/views/TraceViewer.vue +1212 -0
- package/client/src/views/TransitionTimeline.vue +1 -6
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +31 -45
- package/dist/runtime/composables/composable-registry.d.ts +19 -0
- package/dist/runtime/composables/composable-registry.js +63 -5
- package/dist/runtime/composables/render-registry.js +74 -110
- package/dist/runtime/composables/transition-registry.js +103 -28
- package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
- package/dist/runtime/instrumentation/asyncData.js +49 -0
- package/dist/runtime/instrumentation/component.d.ts +2 -0
- package/dist/runtime/instrumentation/component.js +126 -0
- package/dist/runtime/instrumentation/fetch.d.ts +2 -0
- package/dist/runtime/instrumentation/fetch.js +89 -0
- package/dist/runtime/instrumentation/route.d.ts +6 -0
- package/dist/runtime/instrumentation/route.js +41 -0
- package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
- package/dist/runtime/nitro/fetch-capture.js +85 -7
- package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
- package/dist/runtime/nitro/ssr-trace-store.js +84 -0
- package/dist/runtime/plugin.js +82 -2
- package/dist/runtime/tracing/context.d.ts +9 -0
- package/dist/runtime/tracing/context.js +15 -0
- package/dist/runtime/tracing/trace.d.ts +25 -0
- package/dist/runtime/tracing/trace.js +0 -0
- package/dist/runtime/tracing/traceStore.d.ts +39 -0
- package/dist/runtime/tracing/traceStore.js +101 -0
- package/dist/runtime/tracing/tracing.d.ts +27 -0
- package/dist/runtime/tracing/tracing.js +48 -0
- package/package.json +5 -1
- package/client/.env +0 -16
- package/client/dist/assets/index-BCaKoHBH.js +0 -17
- package/client/dist/assets/index-BmGW_M3W.css +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getCurrentTraceId, setCurrentTraceId } from "../tracing/context.js";
|
|
2
|
+
import { traceStore } from "../tracing/traceStore.js";
|
|
3
|
+
export function setupRouteInstrumentation(nuxtApp, options) {
|
|
4
|
+
let activeTraceId;
|
|
5
|
+
const getRoutePath = () => {
|
|
6
|
+
const path = options.getCurrentPath();
|
|
7
|
+
return path && path.length > 0 ? path : "/";
|
|
8
|
+
};
|
|
9
|
+
nuxtApp.hook("page:start", () => {
|
|
10
|
+
if (activeTraceId) {
|
|
11
|
+
traceStore.endTrace(activeTraceId, { status: "cancelled" });
|
|
12
|
+
activeTraceId = void 0;
|
|
13
|
+
}
|
|
14
|
+
const route = getRoutePath();
|
|
15
|
+
const previousRoute = getCurrentTraceId(options.carrier);
|
|
16
|
+
const trace = traceStore.createTrace({
|
|
17
|
+
name: `route:${route}`,
|
|
18
|
+
metadata: {
|
|
19
|
+
kind: "route-navigation",
|
|
20
|
+
route,
|
|
21
|
+
previousTraceId: previousRoute
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
activeTraceId = trace.id;
|
|
25
|
+
setCurrentTraceId(trace.id, options.carrier);
|
|
26
|
+
});
|
|
27
|
+
nuxtApp.hook("page:finish", () => {
|
|
28
|
+
if (!activeTraceId) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const route = getRoutePath();
|
|
32
|
+
traceStore.endTrace(activeTraceId, {
|
|
33
|
+
status: "ok",
|
|
34
|
+
metadata: {
|
|
35
|
+
route
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
setCurrentTraceId(void 0, options.carrier);
|
|
39
|
+
activeTraceId = void 0;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { H3Event } from 'h3';
|
|
2
1
|
interface NitroAppLike {
|
|
3
2
|
hooks: {
|
|
4
|
-
hook: (name:
|
|
3
|
+
hook: (name: string, handler: (...args: unknown[]) => void) => void;
|
|
5
4
|
};
|
|
6
5
|
}
|
|
7
6
|
export default function fetchCapturePlugin(nitroApp: NitroAppLike): void;
|
|
@@ -1,14 +1,92 @@
|
|
|
1
|
-
import { setResponseHeader } from "h3";
|
|
1
|
+
import { getRequestURL, setResponseHeader } from "h3";
|
|
2
|
+
import { addSsrPhaseSpan, createSsrRecord, drainSsrRecord } from "./ssr-trace-store.js";
|
|
3
|
+
let _requestCounter = 0;
|
|
4
|
+
function newRequestId() {
|
|
5
|
+
_requestCounter = (_requestCounter + 1) % 999999;
|
|
6
|
+
return `req_${Date.now()}_${_requestCounter}`;
|
|
7
|
+
}
|
|
2
8
|
export default function fetchCapturePlugin(nitroApp) {
|
|
3
|
-
nitroApp.hooks.hook("request", (
|
|
4
|
-
;
|
|
5
|
-
|
|
9
|
+
nitroApp.hooks.hook("request", (...args) => {
|
|
10
|
+
const event = args[0];
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
event.context.__ssrFetchStart = start;
|
|
13
|
+
let route = "/";
|
|
14
|
+
try {
|
|
15
|
+
route = getRequestURL(event).pathname;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("Error getting request URL:", error);
|
|
18
|
+
}
|
|
19
|
+
const method = String(event.method ?? event.node?.req?.method ?? "GET").toUpperCase();
|
|
20
|
+
const requestId = newRequestId();
|
|
21
|
+
event.context.__observatoryRequestId = requestId;
|
|
22
|
+
createSsrRecord(requestId, route, method);
|
|
23
|
+
globalThis.__observatorySsrContext__ = {
|
|
24
|
+
__observatoryRequestId: requestId,
|
|
25
|
+
__ssrFetchStart: start
|
|
26
|
+
};
|
|
6
27
|
});
|
|
7
|
-
nitroApp.hooks.hook("afterResponse", (
|
|
8
|
-
const
|
|
9
|
-
|
|
28
|
+
nitroApp.hooks.hook("afterResponse", (...args) => {
|
|
29
|
+
const hookStart = performance.now();
|
|
30
|
+
const event = args[0];
|
|
31
|
+
const start = event.context.__ssrFetchStart;
|
|
32
|
+
if (start !== void 0) {
|
|
10
33
|
const ms = Math.round(performance.now() - start);
|
|
11
34
|
setResponseHeader(event, "x-observatory-ssr-ms", String(ms));
|
|
12
35
|
}
|
|
36
|
+
const requestId = event.context.__observatoryRequestId;
|
|
37
|
+
if (requestId) {
|
|
38
|
+
if (start !== void 0) {
|
|
39
|
+
const hookEnd = performance.now();
|
|
40
|
+
addSsrPhaseSpan(requestId, {
|
|
41
|
+
name: "ssr:afterResponse",
|
|
42
|
+
type: "server",
|
|
43
|
+
startMs: Math.max(hookStart - start, 0),
|
|
44
|
+
endMs: Math.max(hookEnd - start, 0),
|
|
45
|
+
metadata: {
|
|
46
|
+
hook: "afterResponse"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const durationMs = start !== void 0 ? Math.max(performance.now() - start, 0) : 0;
|
|
51
|
+
drainSsrRecord(requestId, durationMs);
|
|
52
|
+
}
|
|
53
|
+
const active = globalThis.__observatorySsrContext__;
|
|
54
|
+
if (active?.__observatoryRequestId === requestId) {
|
|
55
|
+
delete globalThis.__observatorySsrContext__;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
nitroApp.hooks.hook("render:html", (...args) => {
|
|
59
|
+
const hookStart = performance.now();
|
|
60
|
+
const html = args[0];
|
|
61
|
+
const ctx = args[1];
|
|
62
|
+
const event = ctx?.event;
|
|
63
|
+
if (!event) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const requestId = event.context.__observatoryRequestId;
|
|
67
|
+
const start = event.context.__ssrFetchStart;
|
|
68
|
+
if (!requestId) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (start !== void 0) {
|
|
72
|
+
const hookEnd = performance.now();
|
|
73
|
+
addSsrPhaseSpan(requestId, {
|
|
74
|
+
name: "ssr:render:html",
|
|
75
|
+
type: "server",
|
|
76
|
+
startMs: Math.max(hookStart - start, 0),
|
|
77
|
+
endMs: Math.max(hookEnd - start, 0),
|
|
78
|
+
metadata: {
|
|
79
|
+
hook: "render:html",
|
|
80
|
+
island: html.island === true
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const durationMs = start !== void 0 ? Math.max(performance.now() - start, 0) : 0;
|
|
85
|
+
const record = drainSsrRecord(requestId, durationMs);
|
|
86
|
+
if (!record) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const json = JSON.stringify(record).replace(/<\/script>/gi, "<\\/script>");
|
|
90
|
+
html.bodyAppend.push(`<script id="__observatory_ssr_spans__" type="application/json">${json}<\/script>`);
|
|
13
91
|
});
|
|
14
92
|
}
|