nuxt-devtools-observatory 0.1.31 → 0.1.33
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 +79 -46
- package/client/.env.example +1 -0
- package/client/dist/assets/index-BqKYgjVB.js +20 -0
- package/client/dist/assets/index-bs1JBJ2u.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +7 -8
- package/client/src/components/SpanInspector.vue +1 -1
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- package/client/src/composables/composable-search.ts +127 -0
- package/client/src/composables/trace-render-aggregation.ts +263 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +1 -5
- package/client/src/composables/useVirtualizationConfig.ts +40 -0
- package/client/src/composables/useVirtualizationFlags.ts +129 -0
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +273 -97
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/ProvideInjectGraph.vue +41 -18
- package/client/src/views/RenderHeatmap.vue +392 -76
- package/client/src/views/TraceViewer.vue +797 -14
- package/client/src/views/TransitionTimeline.vue +112 -19
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -23
- 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 +23 -13
- package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
- package/dist/runtime/instrumentation/fetch.d.ts +7 -1
- package/dist/runtime/instrumentation/fetch.js +22 -1
- 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 +48 -1
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +86 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +18 -3
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.css +0 -1
|
@@ -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,56 @@ 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
|
-
setupFetchInstrumentation(nuxtApp);
|
|
94
|
+
setupFetchInstrumentation(nuxtApp, registries.fetch);
|
|
95
|
+
mergeSsrSpans();
|
|
96
|
+
} else if (config.fetchDashboard) {
|
|
97
|
+
setupFetchInstrumentation(nuxtApp, registries.fetch);
|
|
52
98
|
}
|
|
53
99
|
delete window.__observatory__;
|
|
54
100
|
window.__observatory__ = registries;
|
|
@@ -247,6 +293,7 @@ export default defineNuxtPlugin(() => {
|
|
|
247
293
|
provideInjectGraph: !!registries.provideInject,
|
|
248
294
|
composableTracker: !!registries.composable,
|
|
249
295
|
composableNavigationMode,
|
|
296
|
+
fetchPageSize: typeof config.fetchPageSize === "number" ? config.fetchPageSize : 20,
|
|
250
297
|
renderHeatmap: !!registries.render,
|
|
251
298
|
transitionTracker: !!registries.transition,
|
|
252
299
|
traceViewer: !!config.traceViewer
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ObservatoryTestAPI } from '../../tests/verification/types/observatory.types.js';
|
|
2
|
+
interface VueApp {
|
|
3
|
+
_context?: {
|
|
4
|
+
app?: {
|
|
5
|
+
_component?: unknown;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
__NUXT__?: {
|
|
12
|
+
vueApp?: VueApp;
|
|
13
|
+
};
|
|
14
|
+
__OBSERVATORY_TEST_BRIDGE?: ObservatoryTestAPI;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export declare function injectTestBridge(): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function walkComponentTree(instance, counts) {
|
|
2
|
+
if (!instance) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
const componentName = instance.type?.name ?? instance.type?.__name ?? "Anonymous";
|
|
6
|
+
if (instance.ctx?.__observatoryMountCount) {
|
|
7
|
+
const currentCount = counts.componentMounts[componentName] ?? 0;
|
|
8
|
+
counts.componentMounts[componentName] = currentCount + instance.ctx.__observatoryMountCount;
|
|
9
|
+
}
|
|
10
|
+
if (instance.subTree?.children) {
|
|
11
|
+
instance.subTree.children.forEach((child) => walkComponentTree(child, counts));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function injectTestBridge() {
|
|
15
|
+
if (!import.meta.dev || typeof window === "undefined") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const bridge = {
|
|
19
|
+
async getTraces() {
|
|
20
|
+
const { traceStore } = await import("./tracing/traceStore.js");
|
|
21
|
+
return Array.from(traceStore.entries());
|
|
22
|
+
},
|
|
23
|
+
async getHeatmapData() {
|
|
24
|
+
const { renderRegistry } = await import("./composables/render-registry.js");
|
|
25
|
+
return renderRegistry.getData();
|
|
26
|
+
},
|
|
27
|
+
async getComposableEntries() {
|
|
28
|
+
const { composableRegistry } = await import("./composables/composable-registry.js");
|
|
29
|
+
return composableRegistry.getEntries();
|
|
30
|
+
},
|
|
31
|
+
async getFetchEntries() {
|
|
32
|
+
const { fetchRegistry } = await import("./composables/fetch-registry.js");
|
|
33
|
+
return fetchRegistry.getEntries();
|
|
34
|
+
},
|
|
35
|
+
async getProvideInjectGraph() {
|
|
36
|
+
const { provideInjectRegistry } = await import("./composables/provide-inject-registry.js");
|
|
37
|
+
return provideInjectRegistry.getGraph();
|
|
38
|
+
},
|
|
39
|
+
async getTransitionEntries() {
|
|
40
|
+
const { transitionRegistry } = await import("./composables/transition-registry.js");
|
|
41
|
+
return transitionRegistry.getEntries();
|
|
42
|
+
},
|
|
43
|
+
async getInternalCounts() {
|
|
44
|
+
const counts = {
|
|
45
|
+
componentMounts: {},
|
|
46
|
+
renderOperations: {},
|
|
47
|
+
fetchOperations: {}
|
|
48
|
+
};
|
|
49
|
+
const vueApp = window.__NUXT__?.vueApp;
|
|
50
|
+
if (!vueApp) {
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
walkComponentTree(vueApp._context?.app?._component, counts);
|
|
54
|
+
return counts;
|
|
55
|
+
},
|
|
56
|
+
async clearAllData() {
|
|
57
|
+
const { traceStore } = await import("./tracing/traceStore.js");
|
|
58
|
+
const { renderRegistry } = await import("./composables/render-registry.js");
|
|
59
|
+
const { composableRegistry } = await import("./composables/composable-registry.js");
|
|
60
|
+
const { fetchRegistry } = await import("./composables/fetch-registry.js");
|
|
61
|
+
traceStore.clear();
|
|
62
|
+
renderRegistry.clear();
|
|
63
|
+
composableRegistry.clear();
|
|
64
|
+
fetchRegistry.clear();
|
|
65
|
+
},
|
|
66
|
+
async startRecording() {
|
|
67
|
+
const { traceStore } = await import("./tracing/traceStore.js");
|
|
68
|
+
traceStore.startRecording();
|
|
69
|
+
},
|
|
70
|
+
async stopRecording() {
|
|
71
|
+
const { traceStore } = await import("./tracing/traceStore.js");
|
|
72
|
+
traceStore.stopRecording();
|
|
73
|
+
},
|
|
74
|
+
async exportSnapshot() {
|
|
75
|
+
const snapshot = {
|
|
76
|
+
traces: await this.getTraces(),
|
|
77
|
+
heatmap: await this.getHeatmapData(),
|
|
78
|
+
composables: await this.getComposableEntries(),
|
|
79
|
+
fetches: await this.getFetchEntries(),
|
|
80
|
+
transitions: await this.getTransitionEntries()
|
|
81
|
+
};
|
|
82
|
+
return JSON.stringify(snapshot, null, 2);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
window.__OBSERVATORY_TEST_BRIDGE = bridge;
|
|
86
|
+
}
|
|
@@ -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.33",
|
|
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": [
|
|
@@ -45,14 +49,21 @@
|
|
|
45
49
|
"build": "npm run build:client && nuxt-module-build build",
|
|
46
50
|
"prepack": "npm run build",
|
|
47
51
|
"lint": "eslint .",
|
|
48
|
-
"format": "prettier --write '**/*.{ts,vue,js,json}' --ignore-path .prettierignore && stylelint '**/*.{css,vue}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --fix && eslint --fix '**/*.{ts,vue,js}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**'",
|
|
52
|
+
"format": "prettier --write '**/*.{ts,vue,js,json}' --ignore-path .prettierignore && stylelint '**/*.{css,vue}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**' --fix && eslint --fix '**/*.{ts,vue,js}' --ignore-pattern '**/.nuxt/**' --ignore-pattern '**/.output/**' --ignore-pattern 'coverage/**' --ignore-pattern 'client/dist/**' --ignore-pattern 'scripts/**' --ignore-pattern 'docs/dist/**'",
|
|
49
53
|
"typecheck": "vue-tsc --noEmit",
|
|
50
54
|
"test": "vitest run",
|
|
51
55
|
"test:watch": "vitest",
|
|
52
56
|
"test:coverage": "vitest run --coverage",
|
|
53
57
|
"test:screenshots": "playwright test scripts/playwright/screenshot-trackers.spec.ts",
|
|
54
58
|
"test:e2e": "playwright test scripts/playwright/playground-pages.spec.ts",
|
|
55
|
-
"capture:screenshots": "node scripts/playwright/capture-observatory-screenshots.cjs"
|
|
59
|
+
"capture:screenshots": "node scripts/playwright/capture-observatory-screenshots.cjs",
|
|
60
|
+
"verify": "tsx scripts/run-verification.ts",
|
|
61
|
+
"verify:debug": "playwright test --debug",
|
|
62
|
+
"verify:ui": "playwright test --ui",
|
|
63
|
+
"verify:trace": "playwright test --trace on",
|
|
64
|
+
"verify:report": "playwright show-report",
|
|
65
|
+
"test:accuracy": "vitest run tests/verification/accuracy.test.ts",
|
|
66
|
+
"type-check": "tsc --noEmit --project tests/verification/tsconfig.json"
|
|
56
67
|
},
|
|
57
68
|
"dependencies": {
|
|
58
69
|
"@babel/generator": "^7.29.1",
|
|
@@ -69,11 +80,13 @@
|
|
|
69
80
|
"@nuxt/schema": "^3.0.0",
|
|
70
81
|
"@pinia/nuxt": "^0.11.3",
|
|
71
82
|
"@playwright/test": "^1.58.2",
|
|
83
|
+
"@tanstack/vue-virtual": "^3.13.12",
|
|
72
84
|
"@types/babel__generator": "^7.27.0",
|
|
73
85
|
"@types/babel__traverse": "^7.28.0",
|
|
74
86
|
"@types/node": "^25.5.0",
|
|
75
87
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
76
88
|
"@vitest/coverage-v8": "^4.1.0",
|
|
89
|
+
"@vitest/ui": "^4.1.4",
|
|
77
90
|
"concurrently": "^9.2.1",
|
|
78
91
|
"eslint": "^9.39.2",
|
|
79
92
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -88,10 +101,12 @@
|
|
|
88
101
|
"playwright": "^1.58.2",
|
|
89
102
|
"postcss-html": "^1.8.1",
|
|
90
103
|
"prettier": "^3.8.1",
|
|
104
|
+
"sinon": "^21.1.2",
|
|
91
105
|
"sirv": "^3.0.2",
|
|
92
106
|
"stylelint": "^17.4.0",
|
|
93
107
|
"stylelint-config-standard": "^40.0.0",
|
|
94
108
|
"stylelint-config-standard-vue": "^1.0.0",
|
|
109
|
+
"tsx": "^4.21.0",
|
|
95
110
|
"typescript": "^5.9.3",
|
|
96
111
|
"typescript-eslint": "^8.54.0",
|
|
97
112
|
"unbuild": "^3.6.1",
|
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
|