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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "0.1.31",
7
+ "version": "0.1.32",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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: getSharedKeys(entry.id, entry.name),
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
- return callFn();
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 spansByUid = /* @__PURE__ */ new Map();
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 list = spansByUid.get(uid) ?? [];
84
- list.push(span);
85
- spansByUid.set(uid, list);
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 spans = spansByUid.get(uid) ?? [];
89
- spans.sort((a, b) => a.startTime - b.startTime);
90
- const timeline = spans.slice(-MAX_TIMELINE).map((span) => {
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 = spans.filter((span) => span.metadata?.lifecycle === "mounted").length;
102
- const rerenders = spans.filter((span) => span.metadata?.lifecycle !== "mounted").length;
103
- const totalMs = spans.reduce((sum, span) => sum + (span.durationMs ?? 0), 0);
104
- const eventsCount = Math.max(mountCount + rerenders, 1);
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: '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
  }
@@ -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
+ }
@@ -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.31",
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