nuxt-devtools-observatory 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +117 -30
  2. package/client/.env.example +2 -1
  3. package/client/dist/assets/index-5Wl1XYRH.js +17 -0
  4. package/client/dist/assets/index-DT_QUiIh.css +1 -0
  5. package/client/dist/index.html +2 -2
  6. package/client/src/App.vue +4 -0
  7. package/client/src/components/Flamegraph.vue +442 -0
  8. package/client/src/components/SpanInspector.vue +446 -0
  9. package/client/src/components/TraceFilter.vue +342 -0
  10. package/client/src/components/WaterfallView.vue +443 -0
  11. package/client/src/composables/composable-search.ts +124 -0
  12. package/client/src/composables/trace-render-aggregation.ts +254 -0
  13. package/client/src/composables/useExportImport.ts +63 -0
  14. package/client/src/composables/useTraceFilter.ts +160 -0
  15. package/client/src/stores/observatory.ts +13 -1
  16. package/client/src/views/ComposableTracker.vue +65 -30
  17. package/client/src/views/RenderHeatmap.vue +63 -1
  18. package/client/src/views/TraceViewer.vue +1212 -0
  19. package/client/src/views/TransitionTimeline.vue +1 -6
  20. package/dist/module.d.mts +5 -0
  21. package/dist/module.json +1 -1
  22. package/dist/module.mjs +31 -45
  23. package/dist/runtime/composables/composable-registry.d.ts +19 -0
  24. package/dist/runtime/composables/composable-registry.js +63 -5
  25. package/dist/runtime/composables/render-registry.js +74 -110
  26. package/dist/runtime/composables/transition-registry.js +103 -28
  27. package/dist/runtime/instrumentation/asyncData.d.ts +9 -0
  28. package/dist/runtime/instrumentation/asyncData.js +49 -0
  29. package/dist/runtime/instrumentation/component.d.ts +2 -0
  30. package/dist/runtime/instrumentation/component.js +126 -0
  31. package/dist/runtime/instrumentation/fetch.d.ts +2 -0
  32. package/dist/runtime/instrumentation/fetch.js +89 -0
  33. package/dist/runtime/instrumentation/route.d.ts +6 -0
  34. package/dist/runtime/instrumentation/route.js +41 -0
  35. package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
  36. package/dist/runtime/nitro/fetch-capture.js +85 -7
  37. package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
  38. package/dist/runtime/nitro/ssr-trace-store.js +84 -0
  39. package/dist/runtime/plugin.js +82 -2
  40. package/dist/runtime/tracing/context.d.ts +9 -0
  41. package/dist/runtime/tracing/context.js +15 -0
  42. package/dist/runtime/tracing/trace.d.ts +25 -0
  43. package/dist/runtime/tracing/trace.js +0 -0
  44. package/dist/runtime/tracing/traceStore.d.ts +39 -0
  45. package/dist/runtime/tracing/traceStore.js +101 -0
  46. package/dist/runtime/tracing/tracing.d.ts +27 -0
  47. package/dist/runtime/tracing/tracing.js +48 -0
  48. package/package.json +5 -1
  49. package/client/.env +0 -16
  50. package/client/dist/assets/index-BCaKoHBH.js +0 -17
  51. package/client/dist/assets/index-BmGW_M3W.css +0 -1
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import type { NuxtApp } from '#app';
2
+ export interface RouteInstrumentationOptions {
3
+ getCurrentPath: () => string;
4
+ carrier?: object;
5
+ }
6
+ export declare function setupRouteInstrumentation(nuxtApp: NuxtApp, options: RouteInstrumentationOptions): void;
@@ -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: 'request' | 'afterResponse', handler: (event: H3Event) => void) => void;
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", (event) => {
4
- ;
5
- event.__ssrFetchStart = performance.now();
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", (event) => {
8
- const start = event.__ssrFetchStart;
9
- if (start) {
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
  }