nuxt-devtools-observatory 0.1.31 → 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 +74 -46
- 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/components/Flamegraph.vue +3 -4
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- 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 +1 -5
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +65 -30
- package/client/src/views/RenderHeatmap.vue +63 -1
- package/client/src/views/TraceViewer.vue +618 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- 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 +19 -11
- 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 +44 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +5 -1
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.css +0 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -612,7 +612,7 @@ const module$1 = defineNuxtModule({
|
|
|
612
612
|
if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
|
|
613
613
|
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
614
614
|
}
|
|
615
|
-
if (resolved.fetchDashboard) {
|
|
615
|
+
if (resolved.fetchDashboard || resolved.traceViewer && resolved.instrumentServer) {
|
|
616
616
|
addServerPlugin(resolver.resolve("./runtime/nitro/fetch-capture"));
|
|
617
617
|
}
|
|
618
618
|
const base = "/__observatory";
|
|
@@ -22,6 +22,11 @@ export interface ComposableEntry {
|
|
|
22
22
|
* instances of this composable — indicates module-level (global) state.
|
|
23
23
|
*/
|
|
24
24
|
sharedKeys: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Stable per-composable-name identity group id for each shared key.
|
|
27
|
+
* Keys are ref/reactive key names; values are group ids like `group-1`.
|
|
28
|
+
*/
|
|
29
|
+
sharedKeyGroups?: Record<string, string>;
|
|
25
30
|
watcherCount: number;
|
|
26
31
|
intervalCount: number;
|
|
27
32
|
lifecycle: {
|
|
@@ -39,6 +44,19 @@ export interface ComposableEntry {
|
|
|
39
44
|
/** Whether this composable is called from a layout component (persists across pages). */
|
|
40
45
|
isLayoutComposable?: boolean;
|
|
41
46
|
}
|
|
47
|
+
interface SsrObservatoryEvent {
|
|
48
|
+
context?: {
|
|
49
|
+
__observatoryRequestId?: string;
|
|
50
|
+
__ssrFetchStart?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function __recordSsrComposableSpan(name: string, meta: {
|
|
54
|
+
file: string;
|
|
55
|
+
line: number;
|
|
56
|
+
}, startTime: number, endTime: number, opts?: {
|
|
57
|
+
error?: unknown;
|
|
58
|
+
event?: SsrObservatoryEvent;
|
|
59
|
+
}): void;
|
|
42
60
|
/**
|
|
43
61
|
* Registers a new composable entry, updates an existing one, or retrieves all entries.
|
|
44
62
|
* @remarks The returned object exposes the following methods:
|
|
@@ -78,3 +96,4 @@ export declare function __trackComposable<T>(name: string, callFn: () => T, meta
|
|
|
78
96
|
file: string;
|
|
79
97
|
line: number;
|
|
80
98
|
}): T;
|
|
99
|
+
export {};
|
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
|
|
2
|
+
import { addSsrPhaseSpan } from "../nitro/ssr-trace-store.js";
|
|
3
|
+
function nowMs() {
|
|
4
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
5
|
+
}
|
|
6
|
+
export function __recordSsrComposableSpan(name, meta, startTime, endTime, opts = {}) {
|
|
7
|
+
const eventContext = opts.event?.context ?? globalThis.__observatorySsrContext__;
|
|
8
|
+
const requestId = eventContext?.__observatoryRequestId;
|
|
9
|
+
const requestStart = eventContext?.__ssrFetchStart;
|
|
10
|
+
if (!requestId || typeof requestStart !== "number") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const metadata = {
|
|
14
|
+
file: meta.file,
|
|
15
|
+
line: meta.line,
|
|
16
|
+
phase: "setup"
|
|
17
|
+
};
|
|
18
|
+
if (opts.error instanceof Error) {
|
|
19
|
+
metadata.errorMessage = opts.error.message;
|
|
20
|
+
}
|
|
21
|
+
addSsrPhaseSpan(requestId, {
|
|
22
|
+
name: `composable:${name}`,
|
|
23
|
+
type: "composable",
|
|
24
|
+
startMs: Math.max(startTime - requestStart, 0),
|
|
25
|
+
endMs: Math.max(endTime - requestStart, 0),
|
|
26
|
+
error: !!opts.error,
|
|
27
|
+
metadata
|
|
28
|
+
});
|
|
29
|
+
}
|
|
2
30
|
export function setupComposableRegistry() {
|
|
3
31
|
const entries = /* @__PURE__ */ new Map();
|
|
4
32
|
const liveRefs = /* @__PURE__ */ new Map();
|
|
@@ -38,14 +66,30 @@ export function setupComposableRegistry() {
|
|
|
38
66
|
}
|
|
39
67
|
nameCache = /* @__PURE__ */ new Map();
|
|
40
68
|
sharedKeysCache.set(name, nameCache);
|
|
69
|
+
const identityIds = /* @__PURE__ */ new WeakMap();
|
|
70
|
+
let nextIdentity = 1;
|
|
71
|
+
const getIdentityGroup = (obj) => {
|
|
72
|
+
if (!obj || typeof obj !== "object") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
const target = obj;
|
|
76
|
+
const existing = identityIds.get(target);
|
|
77
|
+
if (existing) {
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
const created = `group-${nextIdentity++}`;
|
|
81
|
+
identityIds.set(target, created);
|
|
82
|
+
return created;
|
|
83
|
+
};
|
|
41
84
|
const peers = [...entries.entries()].filter(([, e]) => e.name === name);
|
|
42
85
|
for (const [eid] of peers) {
|
|
43
86
|
const ownRaw = rawRefs.get(eid);
|
|
44
87
|
if (!ownRaw) {
|
|
45
|
-
nameCache.set(eid, []);
|
|
88
|
+
nameCache.set(eid, { keys: [], groups: {} });
|
|
46
89
|
continue;
|
|
47
90
|
}
|
|
48
91
|
const shared = /* @__PURE__ */ new Set();
|
|
92
|
+
const groups = {};
|
|
49
93
|
for (const [otherId] of peers) {
|
|
50
94
|
if (otherId === eid) {
|
|
51
95
|
continue;
|
|
@@ -57,12 +101,16 @@ export function setupComposableRegistry() {
|
|
|
57
101
|
for (const [key, obj] of Object.entries(ownRaw)) {
|
|
58
102
|
if (key in otherRaw && otherRaw[key] === obj) {
|
|
59
103
|
shared.add(key);
|
|
104
|
+
const identity = getIdentityGroup(obj);
|
|
105
|
+
if (identity) {
|
|
106
|
+
groups[key] = identity;
|
|
107
|
+
}
|
|
60
108
|
}
|
|
61
109
|
}
|
|
62
110
|
}
|
|
63
|
-
nameCache.set(eid, [...shared]);
|
|
111
|
+
nameCache.set(eid, { keys: [...shared], groups });
|
|
64
112
|
}
|
|
65
|
-
return nameCache.get(id) ?? [];
|
|
113
|
+
return nameCache.get(id) ?? { keys: [], groups: {} };
|
|
66
114
|
}
|
|
67
115
|
let currentRoute = "/";
|
|
68
116
|
function setRoute(path) {
|
|
@@ -197,6 +245,7 @@ export function setupComposableRegistry() {
|
|
|
197
245
|
}
|
|
198
246
|
])
|
|
199
247
|
);
|
|
248
|
+
const shared = getSharedKeys(entry.id, entry.name);
|
|
200
249
|
return {
|
|
201
250
|
id: entry.id,
|
|
202
251
|
name: entry.name,
|
|
@@ -207,7 +256,8 @@ export function setupComposableRegistry() {
|
|
|
207
256
|
leakReason: entry.leakReason,
|
|
208
257
|
refs: freshRefs,
|
|
209
258
|
history: entryHistory.get(entry.id) ?? [],
|
|
210
|
-
sharedKeys:
|
|
259
|
+
sharedKeys: shared.keys,
|
|
260
|
+
sharedKeyGroups: shared.groups,
|
|
211
261
|
watcherCount: entry.watcherCount,
|
|
212
262
|
intervalCount: entry.intervalCount,
|
|
213
263
|
lifecycle: entry.lifecycle,
|
|
@@ -318,7 +368,15 @@ export function __trackComposable(name, callFn, meta) {
|
|
|
318
368
|
return callFn();
|
|
319
369
|
}
|
|
320
370
|
if (!import.meta.client) {
|
|
321
|
-
|
|
371
|
+
const startTime = nowMs();
|
|
372
|
+
try {
|
|
373
|
+
const result2 = callFn();
|
|
374
|
+
__recordSsrComposableSpan(name, meta, startTime, nowMs());
|
|
375
|
+
return result2;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
__recordSsrComposableSpan(name, meta, startTime, nowMs(), { error });
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
322
380
|
}
|
|
323
381
|
const registry = window.__observatory__?.composable;
|
|
324
382
|
if (!registry) {
|
|
@@ -8,6 +8,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
8
8
|
const HIDE_INTERNALS = config.heatmapHideInternals ?? false;
|
|
9
9
|
let dirty = true;
|
|
10
10
|
let cachedSnapshot = "[]";
|
|
11
|
+
let resetTimestamp = 0;
|
|
11
12
|
const liveElements = /* @__PURE__ */ new Map();
|
|
12
13
|
function markDirty() {
|
|
13
14
|
dirty = true;
|
|
@@ -61,6 +62,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
function reset() {
|
|
65
|
+
resetTimestamp = performance.now();
|
|
64
66
|
for (const entry of entries.values()) {
|
|
65
67
|
entry.isPersistent = true;
|
|
66
68
|
entry.rerenders = 0;
|
|
@@ -73,21 +75,27 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
73
75
|
}
|
|
74
76
|
function aggregateFromComponentSpans() {
|
|
75
77
|
const componentSpans = traceStore.getAllTraces().flatMap((trace) => trace.spans).filter((span) => span.type === "component");
|
|
76
|
-
const
|
|
78
|
+
const allSpansByUid = /* @__PURE__ */ new Map();
|
|
79
|
+
const postResetSpansByUid = /* @__PURE__ */ new Map();
|
|
77
80
|
for (const span of componentSpans) {
|
|
78
81
|
const uidValue = span.metadata?.uid;
|
|
79
82
|
const uid = typeof uidValue === "number" ? uidValue : Number(uidValue);
|
|
80
83
|
if (!Number.isFinite(uid)) {
|
|
81
84
|
continue;
|
|
82
85
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
const allList = allSpansByUid.get(uid) ?? [];
|
|
87
|
+
allList.push(span);
|
|
88
|
+
allSpansByUid.set(uid, allList);
|
|
89
|
+
if (span.startTime >= resetTimestamp) {
|
|
90
|
+
const postList = postResetSpansByUid.get(uid) ?? [];
|
|
91
|
+
postList.push(span);
|
|
92
|
+
postResetSpansByUid.set(uid, postList);
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
for (const [uid, entry] of entries.entries()) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const timeline =
|
|
96
|
+
const allSpans = (allSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
|
|
97
|
+
const postResetSpans = (postResetSpansByUid.get(uid) ?? []).sort((a, b) => a.startTime - b.startTime);
|
|
98
|
+
const timeline = postResetSpans.slice(-MAX_TIMELINE).map((span) => {
|
|
91
99
|
const lifecycle = span.metadata?.lifecycle === "mounted" ? "mount" : "update";
|
|
92
100
|
const routeValue = span.metadata?.route;
|
|
93
101
|
const route = typeof routeValue === "string" && routeValue.length > 0 ? routeValue : entry.route;
|
|
@@ -98,10 +106,10 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
|
|
|
98
106
|
route
|
|
99
107
|
};
|
|
100
108
|
});
|
|
101
|
-
const mountCount =
|
|
102
|
-
const rerenders =
|
|
103
|
-
const totalMs =
|
|
104
|
-
const eventsCount = Math.max(
|
|
109
|
+
const mountCount = allSpans.filter((span) => span.metadata?.lifecycle === "mounted").length;
|
|
110
|
+
const rerenders = postResetSpans.filter((span) => span.metadata?.lifecycle !== "mounted").length;
|
|
111
|
+
const totalMs = postResetSpans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
|
|
112
|
+
const eventsCount = Math.max(postResetSpans.length, 1);
|
|
105
113
|
entry.mountCount = mountCount;
|
|
106
114
|
entry.rerenders = rerenders;
|
|
107
115
|
entry.totalMs = Math.round(totalMs * 10) / 10;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request SSR trace collector.
|
|
3
|
+
*
|
|
4
|
+
* Each incoming SSR page request gets its own record keyed by a unique
|
|
5
|
+
* requestId (stored on H3 `event.context`). Spans are accumulated during the
|
|
6
|
+
* request lifecycle and drained once the HTML is rendered (via the
|
|
7
|
+
* `render:html` Nitro hook), so they can be injected into the page as inline
|
|
8
|
+
* JSON for the client plugin to pick up.
|
|
9
|
+
*/
|
|
10
|
+
export interface SsrSpan {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
type: string;
|
|
14
|
+
/** Milliseconds relative to request start (0 = request began). */
|
|
15
|
+
startTime: number;
|
|
16
|
+
endTime?: number;
|
|
17
|
+
durationMs?: number;
|
|
18
|
+
status: 'ok' | 'error' | 'active';
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface SsrTraceRecord {
|
|
22
|
+
traceId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
spans: SsrSpan[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Open a new per-request SSR record. A `navigation` span covering the full
|
|
28
|
+
* request is pre-populated; its end time is filled in when `drainSsrRecord`
|
|
29
|
+
* is called.
|
|
30
|
+
* @param {string} requestId - Unique identifier for the HTTP request, stored in `event.context`.
|
|
31
|
+
* @param {string} route - Request pathname (e.g. `/dashboard`).
|
|
32
|
+
* @param {string} method - HTTP method in upper-case (e.g. `GET`).
|
|
33
|
+
* @returns {SsrTraceRecord} The newly created `SsrTraceRecord` keyed by `requestId`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createSsrRecord(requestId: string, route: string, method: string): SsrTraceRecord;
|
|
36
|
+
/**
|
|
37
|
+
* Append an SSR-side fetch span. `startMs` / `endMs` are milliseconds
|
|
38
|
+
* relative to request start (same origin as `createSsrRecord`).
|
|
39
|
+
* @param {string} requestId - The request identifier returned by `createSsrRecord`.
|
|
40
|
+
* @param {object} opts - Span options.
|
|
41
|
+
* @param {string} opts.url - The request URL or path that was fetched.
|
|
42
|
+
* @param {string} opts.method - HTTP method in upper-case (e.g. `GET`).
|
|
43
|
+
* @param {number} opts.startMs - Span start, in ms relative to the request start time.
|
|
44
|
+
* @param {number} opts.endMs - Span end, in ms relative to the request start time.
|
|
45
|
+
* @param {number} [opts.statusCode] - Optional HTTP response status code.
|
|
46
|
+
* @param {boolean} [opts.error] - Set to `true` to mark the span status as `error`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function addSsrFetchSpan(requestId: string, opts: {
|
|
49
|
+
url: string;
|
|
50
|
+
method: string;
|
|
51
|
+
startMs: number;
|
|
52
|
+
endMs: number;
|
|
53
|
+
statusCode?: number;
|
|
54
|
+
error?: boolean;
|
|
55
|
+
}): void;
|
|
56
|
+
/**
|
|
57
|
+
* Append a generic SSR phase span (e.g. `render:html`, `afterResponse`) to
|
|
58
|
+
* an existing request record.
|
|
59
|
+
* @param {string} requestId - The request identifier returned by `createSsrRecord`.
|
|
60
|
+
* @param {object} opts - Span options.
|
|
61
|
+
* @param {string} opts.name - Human-readable span name.
|
|
62
|
+
* @param {string} [opts.type] - Span type. Defaults to `server`.
|
|
63
|
+
* @param {number} opts.startMs - Span start, in ms relative to request start.
|
|
64
|
+
* @param {number} opts.endMs - Span end, in ms relative to request start.
|
|
65
|
+
* @param {boolean} [opts.error] - Set to `true` to mark the span status as `error`.
|
|
66
|
+
* @param {Record<string, unknown>} [opts.metadata] - Additional metadata fields.
|
|
67
|
+
*/
|
|
68
|
+
export declare function addSsrPhaseSpan(requestId: string, opts: {
|
|
69
|
+
name: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
startMs: number;
|
|
72
|
+
endMs: number;
|
|
73
|
+
error?: boolean;
|
|
74
|
+
metadata?: Record<string, unknown>;
|
|
75
|
+
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* Finalize and remove the record for `requestId`. The pre-populated
|
|
78
|
+
* navigation span is closed with `durationMs`. Returns `undefined` if the
|
|
79
|
+
* requestId is unknown (e.g. non-page requests that never called
|
|
80
|
+
* `createSsrRecord`).
|
|
81
|
+
* @param {string} requestId - The request identifier returned by `createSsrRecord`.
|
|
82
|
+
* @param {number} durationMs - Total SSR request duration in milliseconds, used to close the navigation span.
|
|
83
|
+
* @returns {SsrTraceRecord | undefined} The completed `SsrTraceRecord`, or `undefined` if no record exists for `requestId`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function drainSsrRecord(requestId: string, durationMs: number): SsrTraceRecord | undefined;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2
|
+
let _counter = 0;
|
|
3
|
+
function newId(prefix) {
|
|
4
|
+
_counter = (_counter + 1) % 999999;
|
|
5
|
+
return `${prefix}_ssr_${Date.now()}_${_counter}`;
|
|
6
|
+
}
|
|
7
|
+
export function createSsrRecord(requestId, route, method) {
|
|
8
|
+
const record = {
|
|
9
|
+
traceId: newId("trace"),
|
|
10
|
+
name: `ssr:${route}`,
|
|
11
|
+
spans: [
|
|
12
|
+
{
|
|
13
|
+
id: newId("span"),
|
|
14
|
+
name: "ssr:navigation",
|
|
15
|
+
type: "navigation",
|
|
16
|
+
startTime: 0,
|
|
17
|
+
status: "active",
|
|
18
|
+
metadata: {
|
|
19
|
+
origin: "ssr",
|
|
20
|
+
route,
|
|
21
|
+
method
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
pending.set(requestId, record);
|
|
27
|
+
return record;
|
|
28
|
+
}
|
|
29
|
+
export function addSsrFetchSpan(requestId, opts) {
|
|
30
|
+
const record = pending.get(requestId);
|
|
31
|
+
if (!record) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const durationMs = Math.max(opts.endMs - opts.startMs, 0);
|
|
35
|
+
record.spans.push({
|
|
36
|
+
id: newId("span"),
|
|
37
|
+
name: opts.url,
|
|
38
|
+
type: "fetch",
|
|
39
|
+
startTime: opts.startMs,
|
|
40
|
+
endTime: opts.endMs,
|
|
41
|
+
durationMs,
|
|
42
|
+
status: opts.error ? "error" : "ok",
|
|
43
|
+
metadata: {
|
|
44
|
+
origin: "ssr",
|
|
45
|
+
url: opts.url,
|
|
46
|
+
method: opts.method,
|
|
47
|
+
statusCode: opts.statusCode
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function addSsrPhaseSpan(requestId, opts) {
|
|
52
|
+
const record = pending.get(requestId);
|
|
53
|
+
if (!record) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const durationMs = Math.max(opts.endMs - opts.startMs, 0);
|
|
57
|
+
record.spans.push({
|
|
58
|
+
id: newId("span"),
|
|
59
|
+
name: opts.name,
|
|
60
|
+
type: opts.type ?? "server",
|
|
61
|
+
startTime: opts.startMs,
|
|
62
|
+
endTime: opts.endMs,
|
|
63
|
+
durationMs,
|
|
64
|
+
status: opts.error ? "error" : "ok",
|
|
65
|
+
metadata: {
|
|
66
|
+
origin: "ssr",
|
|
67
|
+
...opts.metadata ?? {}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function drainSsrRecord(requestId, durationMs) {
|
|
72
|
+
const record = pending.get(requestId);
|
|
73
|
+
pending.delete(requestId);
|
|
74
|
+
if (!record) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
const navSpan = record.spans[0];
|
|
78
|
+
if (navSpan) {
|
|
79
|
+
navSpan.endTime = durationMs;
|
|
80
|
+
navSpan.durationMs = durationMs;
|
|
81
|
+
navSpan.status = "ok";
|
|
82
|
+
}
|
|
83
|
+
return record;
|
|
84
|
+
}
|
package/dist/runtime/plugin.js
CHANGED
|
@@ -45,10 +45,54 @@ export default defineNuxtPlugin(() => {
|
|
|
45
45
|
if (config.transitionTracker) {
|
|
46
46
|
registries.transition = setupTransitionRegistry();
|
|
47
47
|
}
|
|
48
|
+
function mergeSsrSpans() {
|
|
49
|
+
if (!import.meta.client) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const el = document.getElementById("__observatory_ssr_spans__");
|
|
53
|
+
if (!el) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let record;
|
|
57
|
+
try {
|
|
58
|
+
record = JSON.parse(el.textContent ?? "");
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!record?.traceId || !Array.isArray(record.spans)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const navDurationMs = record.spans[0]?.durationMs ?? 0;
|
|
66
|
+
const traceStartTime = performance.now() - navDurationMs;
|
|
67
|
+
traceStore.createTrace({
|
|
68
|
+
id: record.traceId,
|
|
69
|
+
name: record.name,
|
|
70
|
+
startTime: traceStartTime,
|
|
71
|
+
metadata: { origin: "ssr" }
|
|
72
|
+
});
|
|
73
|
+
for (const span of record.spans) {
|
|
74
|
+
traceStore.addSpan({
|
|
75
|
+
id: span.id,
|
|
76
|
+
traceId: record.traceId,
|
|
77
|
+
name: span.name,
|
|
78
|
+
type: span.type,
|
|
79
|
+
startTime: traceStartTime + span.startTime,
|
|
80
|
+
endTime: span.endTime !== void 0 ? traceStartTime + span.endTime : void 0,
|
|
81
|
+
status: span.status,
|
|
82
|
+
metadata: { ...span.metadata ?? {}, origin: "ssr" }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
traceStore.endTrace(record.traceId, {
|
|
86
|
+
endTime: traceStartTime + navDurationMs,
|
|
87
|
+
status: "ok",
|
|
88
|
+
metadata: { origin: "ssr" }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
48
91
|
if (import.meta.client) {
|
|
49
92
|
if (config.traceViewer) {
|
|
50
93
|
setupComponentInstrumentation(nuxtApp);
|
|
51
94
|
setupFetchInstrumentation(nuxtApp);
|
|
95
|
+
mergeSsrSpans();
|
|
52
96
|
}
|
|
53
97
|
delete window.__observatory__;
|
|
54
98
|
window.__observatory__ = registries;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SpanType = 'render' | 'transition' | 'fetch' | 'composable' | 'navigation' | 'custom' | (string & {});
|
|
1
|
+
export type SpanType = 'render' | 'component' | 'transition' | 'fetch' | 'composable' | 'navigation' | 'custom' | (string & {});
|
|
2
2
|
export type SpanStatus = 'active' | 'ok' | 'error' | 'cancelled';
|
|
3
3
|
export interface Span {
|
|
4
4
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-devtools-observatory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Nuxt DevTools: useFetch Dashboard, provide/inject Graph, Composable Tracker, Render Heatmap",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"./runtime/fetch-registry": {
|
|
28
28
|
"types": "./dist/runtime/composables/fetch-registry.d.ts",
|
|
29
29
|
"import": "./dist/runtime/composables/fetch-registry.js"
|
|
30
|
+
},
|
|
31
|
+
"./runtime/async-data-instrumentation": {
|
|
32
|
+
"types": "./dist/runtime/instrumentation/asyncData.d.ts",
|
|
33
|
+
"import": "./dist/runtime/instrumentation/asyncData.js"
|
|
30
34
|
}
|
|
31
35
|
},
|
|
32
36
|
"files": [
|
package/client/.env
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# VITE_Observatory registry/configurable limits
|
|
2
|
-
VITE_OBSERVATORY_FETCH_DASHBOARD=true
|
|
3
|
-
VITE_OBSERVATORY_PROVIDE_INJECT_GRAPH=true
|
|
4
|
-
VITE_OBSERVATORY_COMPOSABLE_TRACKER=true
|
|
5
|
-
VITE_OBSERVATORY_RENDER_HEATMAP=true
|
|
6
|
-
VITE_OBSERVATORY_TRANSITION_TRACKER=true
|
|
7
|
-
VITE_OBSERVATORY_TRACE_VIEWER=true
|
|
8
|
-
VITE_OBSERVATORY_MAX_FETCH_ENTRIES=200
|
|
9
|
-
VITE_OBSERVATORY_MAX_PAYLOAD_BYTES=10000
|
|
10
|
-
VITE_OBSERVATORY_MAX_TRANSITIONS=500
|
|
11
|
-
VITE_OBSERVATORY_MAX_COMPOSABLE_HISTORY=50
|
|
12
|
-
VITE_OBSERVATORY_MAX_COMPOSABLE_ENTRIES=300
|
|
13
|
-
VITE_OBSERVATORY_MAX_RENDER_TIMELINE=100
|
|
14
|
-
VITE_OBSERVATORY_HEATMAP_THRESHOLD_COUNT=3
|
|
15
|
-
VITE_OBSERVATORY_HEATMAP_THRESHOLD_TIME=1600
|
|
16
|
-
VITE_OBSERVATORY_HEATMAP_HIDE_INTERNALS=true
|
|
17
|
-
VITE_OBSERVATORY_INSTRUMENT_SERVER=true
|