nuxt-devtools-observatory 0.1.19 → 0.1.22

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 CHANGED
@@ -14,18 +14,49 @@ Nuxt DevTools extension providing five missing observability features:
14
14
  pnpm add nuxt-devtools-observatory
15
15
  ```
16
16
 
17
+ You can configure all observability features and limits from your consuming project's `nuxt.config.ts` or `.env` file. All options in `.env.example` are supported as either environment variables or as properties under the `observatory` key in your Nuxt config. Options set in `nuxt.config.ts` take precedence over `.env` values.
18
+
19
+ **Available options:**
20
+
21
+ - `instrumentServer` (boolean) — Instrument the server for SSR/Nitro fetch and composable tracking (set via `OBSERVATORY_INSTRUMENT_SERVER` or `VITE_OBSERVATORY_INSTRUMENT_SERVER`)
22
+ - `fetchDashboard` (boolean) — Enable useFetch dashboard
23
+ - `provideInjectGraph` (boolean) — Enable provide/inject graph
24
+ - `composableTracker` (boolean) — Enable composable tracker
25
+ - `renderHeatmap` (boolean) — Enable render heatmap
26
+ - `transitionTracker` (boolean) — Enable transition tracker
27
+ - `heatmapThresholdCount` (number) — Highlight components with N+ renders in heatmap
28
+ - `heatmapThresholdTime` (number) — Highlight components with render time above this (ms)
29
+ - `heatmapHideInternals` (boolean) — Hide node_modules and internal components in the render heatmap for a cleaner view
30
+ - `maxFetchEntries` (number) — Max fetch entries to keep in memory
31
+ - `maxPayloadBytes` (number) — Max payload size (bytes) per fetch entry
32
+ - `maxTransitions` (number) — Max transition entries to keep in memory
33
+ - `maxComposableHistory` (number) — Max composable history events per entry
34
+ - `maxComposableEntries` (number) — Max composable entries to keep in memory
35
+ - `maxRenderTimeline` (number) — Max render timeline events per entry
36
+
37
+ See `.env.example` for all environment variable names.
38
+
17
39
  ```ts
18
40
  // nuxt.config.ts
19
41
  export default defineNuxtConfig({
20
42
  modules: ['nuxt-devtools-observatory'],
21
43
 
22
44
  observatory: {
23
- fetchDashboard: true,
24
- provideInjectGraph: true,
25
- composableTracker: true,
26
- renderHeatmap: true,
27
- transitionTracker: true,
28
- heatmapThresholdCount: 5, // highlight components with 5+ renders
45
+ instrumentServer: true, // Instrument the server for SSR/Nitro fetch and composable tracking. Enable this when using SSR so server-side composable calls are captured. Disable for SPA projects to avoid double-registration caused by the transform running on both builds.
46
+ fetchDashboard: true, // Enable useFetch dashboard
47
+ provideInjectGraph: true, // Enable provide/inject graph
48
+ composableTracker: true, // Enable composable tracker
49
+ renderHeatmap: true, // Enable render heatmap
50
+ transitionTracker: true, // Enable transition tracker
51
+ heatmapThresholdCount: 5, // Highlight components with 5+ renders
52
+ heatmapThresholdTime: 1600, // Highlight components with render time above this (ms)
53
+ heatmapHideInternals: true, // Hide node_modules and internal components in the render heatmap
54
+ maxFetchEntries: 200, // Max fetch entries to keep in memory
55
+ maxPayloadBytes: 10000, // Max payload size (bytes) per fetch entry
56
+ maxTransitions: 500, // Max transition entries to keep in memory
57
+ maxComposableHistory: 50, // Max composable history events per entry
58
+ maxComposableEntries: 300, // Max composable entries to keep in memory
59
+ maxRenderTimeline: 100, // Max render timeline events per entry
29
60
  },
30
61
 
31
62
  devtools: { enabled: true },
package/client/.env CHANGED
@@ -12,3 +12,5 @@ VITE_OBSERVATORY_MAX_COMPOSABLE_ENTRIES=300
12
12
  VITE_OBSERVATORY_MAX_RENDER_TIMELINE=100
13
13
  VITE_OBSERVATORY_HEATMAP_THRESHOLD_COUNT=3
14
14
  VITE_OBSERVATORY_HEATMAP_THRESHOLD_TIME=1600
15
+ VITE_OBSERVATORY_HEATMAP_HIDE_INTERNALS=true
16
+ VITE_OBSERVATORY_INSTRUMENT_SERVER=true
@@ -12,3 +12,5 @@ VITE_OBSERVATORY_MAX_COMPOSABLE_ENTRIES=300
12
12
  VITE_OBSERVATORY_MAX_RENDER_TIMELINE=100
13
13
  VITE_OBSERVATORY_HEATMAP_THRESHOLD_COUNT=3
14
14
  VITE_OBSERVATORY_HEATMAP_THRESHOLD_TIME=1600
15
+ VITE_OBSERVATORY_HEATMAP_HIDE_INTERNALS=true
16
+ VITE_OBSERVATORY_INSTRUMENT_SERVER=true
package/dist/module.d.mts CHANGED
@@ -1,6 +1,14 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
+ /**
5
+ * Instrument composables, provide/inject, fetch, and transitions on the
6
+ * server build as well as the client build. Enable this when using SSR so
7
+ * server-side composable calls are captured. Disable for SPA projects to
8
+ * avoid double-registration caused by the transform running on both builds.
9
+ * @default false
10
+ */
11
+ instrumentServer?: boolean;
4
12
  /**
5
13
  * Maximum number of fetch entries to keep in memory
6
14
  * @default 200
@@ -56,6 +64,11 @@ interface ModuleOptions {
56
64
  * @default true
57
65
  */
58
66
  transitionTracker?: boolean;
67
+ /**
68
+ * Hide node_modules/internal components in the render heatmap
69
+ * @default false
70
+ */
71
+ heatmapHideInternals?: boolean;
59
72
  /**
60
73
  * Minimum render count / ms threshold to highlight in the heatmap
61
74
  * @default 3
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.19",
7
+ "version": "0.1.22",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -285,21 +285,42 @@ const traverse = _traverse.default ?? _traverse;
285
285
  const generate = _generate.default ?? _generate;
286
286
  const COMPOSABLE_RE = /\buse[A-Z]/;
287
287
  const SKIP_LIST = /* @__PURE__ */ new Set([
288
+ // useFetch family — tracked by the fetch dashboard
288
289
  "useFetch",
289
290
  "useAsyncData",
290
291
  "useLazyFetch",
291
292
  "useLazyAsyncData",
292
- "useState",
293
- "useRoute",
294
- "useRouter",
293
+ // Nuxt auto-imports
294
+ "useCookie",
295
+ "useRequestEvent",
296
+ "useRequestHeaders",
297
+ "useRequestURL",
298
+ "useResponseHeader",
295
299
  "useNuxtApp",
296
300
  "useRuntimeConfig",
301
+ "useRoute",
302
+ "useRouter",
303
+ "useNuxtData",
304
+ "useError",
305
+ "useState",
306
+ "useAppConfig",
307
+ // Nuxt head
297
308
  "useHead",
298
309
  "useSeoMeta",
299
310
  "useServerSeoMeta",
300
- "useNuxtData",
301
- "useError",
302
- "useRequestHeaders"
311
+ "useHeadSafe",
312
+ // Nuxt i18n (common plugin)
313
+ "useI18n",
314
+ "useLocalePath",
315
+ "useLocaleRoute",
316
+ // Vue built-ins
317
+ "useSlots",
318
+ "useAttrs",
319
+ "useModel",
320
+ "useTemplateRef",
321
+ "useId",
322
+ "useCssModule",
323
+ "useCssVars"
303
324
  ]);
304
325
  function composableTrackerPlugin() {
305
326
  return {
@@ -345,6 +366,14 @@ function composableTrackerPlugin() {
345
366
  if (SKIP_LIST.has(name)) {
346
367
  return;
347
368
  }
369
+ const binding = path.scope.getBinding(name);
370
+ if (binding?.path.isImportSpecifier() || binding?.path.isImportDefaultSpecifier()) {
371
+ const importDecl = binding.path.parentPath?.node;
372
+ const source = importDecl?.source?.value ?? "";
373
+ if (source && !source.startsWith(".") && !source.startsWith("/")) {
374
+ return;
375
+ }
376
+ }
348
377
  let parent = path.parentPath;
349
378
  let isWrapped = false;
350
379
  while (parent) {
@@ -385,11 +414,13 @@ function composableTrackerPlugin() {
385
414
  const importLine = `import { __trackComposable } from 'nuxt-devtools-observatory/runtime/composable-registry';
386
415
  `;
387
416
  const output = generate(ast, { retainLines: true }, scriptCode);
417
+ const alreadyImported = output.code.includes("nuxt-devtools-observatory/runtime/composable-registry");
418
+ const prefix = alreadyImported ? "" : importLine;
388
419
  let finalCode;
389
420
  if (isVue) {
390
- finalCode = code.slice(0, scriptStart) + (scriptCode.includes("__trackComposable") ? "" : importLine) + output.code + code.slice(scriptStart + scriptCode.length);
421
+ finalCode = code.slice(0, scriptStart) + prefix + output.code + code.slice(scriptStart + scriptCode.length);
391
422
  } else {
392
- finalCode = (scriptCode.includes("__trackComposable") ? "" : importLine) + output.code;
423
+ finalCode = prefix + output.code;
393
424
  }
394
425
  return { code: finalCode, map: output.map };
395
426
  } catch (err) {
@@ -519,11 +550,12 @@ const module$1 = defineNuxtModule({
519
550
  compatibility: { nuxt: "^3.0.0 || ^4.0.0" }
520
551
  },
521
552
  defaults: {
522
- fetchDashboard: true,
523
- provideInjectGraph: true,
524
- composableTracker: true,
525
- renderHeatmap: true,
526
- transitionTracker: true,
553
+ instrumentServer: process.env.OBSERVATORY_INSTRUMENT_SERVER === "true",
554
+ fetchDashboard: process.env.OBSERVATORY_FETCH_DASHBOARD === "true",
555
+ provideInjectGraph: process.env.OBSERVATORY_PROVIDE_INJECT_GRAPH === "true",
556
+ composableTracker: process.env.OBSERVATORY_COMPOSABLE_TRACKER === "true",
557
+ renderHeatmap: process.env.OBSERVATORY_RENDER_HEATMAP === "true",
558
+ transitionTracker: process.env.OBSERVATORY_TRANSITION_TRACKER === "true",
527
559
  heatmapThresholdCount: process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3,
528
560
  heatmapThresholdTime: process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600,
529
561
  maxFetchEntries: process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200,
@@ -531,7 +563,8 @@ const module$1 = defineNuxtModule({
531
563
  maxTransitions: process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500,
532
564
  maxComposableHistory: process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50,
533
565
  maxComposableEntries: process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300,
534
- maxRenderTimeline: process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100
566
+ maxRenderTimeline: process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100,
567
+ heatmapHideInternals: process.env.OBSERVATORY_HEATMAP_HIDE_INTERNALS === "true"
535
568
  },
536
569
  setup(options, nuxt) {
537
570
  if (!nuxt.options.dev) {
@@ -542,11 +575,16 @@ const module$1 = defineNuxtModule({
542
575
  }
543
576
  const resolver = createResolver(import.meta.url);
544
577
  const resolved = {
578
+ ...defaults,
579
+ ...options,
580
+ // Allow runtime overrides via env
581
+ heatmapHideInternals: typeof process.env.OBSERVATORY_HEATMAP_HIDE_INTERNALS !== "undefined" ? process.env.OBSERVATORY_HEATMAP_HIDE_INTERNALS === "true" : typeof options.heatmapHideInternals !== "undefined" ? options.heatmapHideInternals : defaults.heatmapHideInternals,
545
582
  fetchDashboard: options.fetchDashboard ?? (process.env.OBSERVATORY_FETCH_DASHBOARD ? process.env.OBSERVATORY_FETCH_DASHBOARD === "true" : true),
546
583
  provideInjectGraph: options.provideInjectGraph ?? (process.env.OBSERVATORY_PROVIDE_INJECT_GRAPH ? process.env.OBSERVATORY_PROVIDE_INJECT_GRAPH === "true" : true),
547
584
  composableTracker: options.composableTracker ?? (process.env.OBSERVATORY_COMPOSABLE_TRACKER ? process.env.OBSERVATORY_COMPOSABLE_TRACKER === "true" : true),
548
585
  renderHeatmap: options.renderHeatmap ?? (process.env.OBSERVATORY_RENDER_HEATMAP ? process.env.OBSERVATORY_RENDER_HEATMAP === "true" : true),
549
586
  transitionTracker: options.transitionTracker ?? (process.env.OBSERVATORY_TRANSITION_TRACKER ? process.env.OBSERVATORY_TRANSITION_TRACKER === "true" : true),
587
+ instrumentServer: options.instrumentServer ?? (process.env.OBSERVATORY_INSTRUMENT_SERVER ? process.env.OBSERVATORY_INSTRUMENT_SERVER === "true" : false),
550
588
  heatmapThresholdCount: options.heatmapThresholdCount ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3),
551
589
  heatmapThresholdTime: options.heatmapThresholdTime ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600),
552
590
  maxFetchEntries: options.maxFetchEntries ?? (process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200),
@@ -566,17 +604,18 @@ const module$1 = defineNuxtModule({
566
604
  aliases["nuxt-devtools-observatory/runtime/fetch-registry"] = resolver.resolve("./runtime/composables/fetch-registry");
567
605
  config.resolve = { ...config.resolve, alias: aliases };
568
606
  });
607
+ const vitePluginScope = resolved.instrumentServer ? { server: true, client: true } : { server: false, client: true };
569
608
  if (resolved.fetchDashboard) {
570
- addVitePlugin(fetchInstrumentPlugin());
609
+ addVitePlugin(fetchInstrumentPlugin(), vitePluginScope);
571
610
  }
572
611
  if (resolved.provideInjectGraph) {
573
- addVitePlugin(provideInjectPlugin());
612
+ addVitePlugin(provideInjectPlugin(), vitePluginScope);
574
613
  }
575
614
  if (resolved.composableTracker) {
576
- addVitePlugin(composableTrackerPlugin());
615
+ addVitePlugin(composableTrackerPlugin(), vitePluginScope);
577
616
  }
578
617
  if (resolved.transitionTracker) {
579
- addVitePlugin(transitionTrackerPlugin());
618
+ addVitePlugin(transitionTrackerPlugin(), vitePluginScope);
580
619
  }
581
620
  if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
582
621
  addPlugin(resolver.resolve("./runtime/plugin"));
@@ -630,8 +669,7 @@ ${configScript}`);
630
669
  }
631
670
  });
632
671
  nuxt.options.runtimeConfig.public.observatory = {
633
- heatmapThresholdCount: resolved.heatmapThresholdCount,
634
- heatmapThresholdTime: resolved.heatmapThresholdTime,
672
+ instrumentServer: resolved.instrumentServer,
635
673
  clientOrigin,
636
674
  fetchDashboard: resolved.fetchDashboard,
637
675
  provideInjectGraph: resolved.provideInjectGraph,
@@ -643,7 +681,10 @@ ${configScript}`);
643
681
  maxTransitions: resolved.maxTransitions,
644
682
  maxComposableHistory: resolved.maxComposableHistory,
645
683
  maxComposableEntries: resolved.maxComposableEntries,
646
- maxRenderTimeline: resolved.maxRenderTimeline
684
+ maxRenderTimeline: resolved.maxRenderTimeline,
685
+ heatmapHideInternals: resolved.heatmapHideInternals,
686
+ heatmapThresholdCount: resolved.heatmapThresholdCount,
687
+ heatmapThresholdTime: resolved.heatmapThresholdTime
647
688
  };
648
689
  }
649
690
  });
@@ -298,7 +298,7 @@ export function __trackComposable(name, callFn, meta) {
298
298
  return callFn();
299
299
  }
300
300
  const instance = getCurrentInstance();
301
- const id = `${name}::${instance?.uid ?? "global"}::${meta.file}:${meta.line}::${Date.now()}::${Math.random().toString(36).slice(2, 7)}`;
301
+ const id = instance ? `${name}::${instance.uid}::${meta.file}:${meta.line}::${Date.now()}::${Math.random().toString(36).slice(2, 7)}` : `${name}::global::${meta.file}:${meta.line}`;
302
302
  const trackedIntervals = [];
303
303
  const clearedIntervals = /* @__PURE__ */ new Set();
304
304
  const alreadyPatched = !!window.setInterval.__obs;
@@ -373,7 +373,17 @@ export function __trackComposable(name, callFn, meta) {
373
373
  line: meta.line,
374
374
  route: registry.getRoute()
375
375
  };
376
- registry.register(entry);
376
+ if (!instance && registry.getAll().some((e) => e.id === id)) {
377
+ registry.update(id, {
378
+ status: "mounted",
379
+ refs,
380
+ watcherCount: trackedWatchers.length,
381
+ intervalCount: trackedIntervals.length,
382
+ route: registry.getRoute()
383
+ });
384
+ } else {
385
+ registry.register(entry);
386
+ }
377
387
  if (instance) {
378
388
  registry.registerLiveRefs(id, liveRefMap);
379
389
  registry.registerRawRefs(id, rawRefMap);
@@ -1,9 +1,12 @@
1
+ import { useRuntimeConfig } from "#app";
1
2
  export function setupRenderRegistry(nuxtApp, options = {}) {
2
3
  const entries = /* @__PURE__ */ new Map();
3
4
  const pendingTriggeredRenders = /* @__PURE__ */ new Set();
4
5
  const renderStartTimes = /* @__PURE__ */ new Map();
5
6
  let currentRoute = "/";
6
- const MAX_TIMELINE = typeof process !== "undefined" && process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100;
7
+ const config = useRuntimeConfig().public.observatory;
8
+ const MAX_TIMELINE = config.maxRenderTimeline ?? 100;
9
+ const HIDE_INTERNALS = config.heatmapHideInternals ?? false;
7
10
  let dirty = true;
8
11
  let cachedSnapshot = "[]";
9
12
  function markDirty() {
@@ -12,10 +15,28 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
12
15
  function setRoute(path) {
13
16
  currentRoute = path;
14
17
  }
18
+ function isInternalInstance(instance) {
19
+ const file = instance.$.type.__file;
20
+ return !file || file.includes("node_modules");
21
+ }
22
+ function nearestTrackedAncestorUid(instance) {
23
+ let cursor = instance.$parent;
24
+ while (cursor) {
25
+ if (!isInternalInstance(cursor)) {
26
+ return cursor.$.uid;
27
+ }
28
+ cursor = cursor.$parent;
29
+ }
30
+ return void 0;
31
+ }
15
32
  function ensureEntry(instance) {
16
33
  const uid = instance.$.uid;
17
34
  if (!entries.has(uid)) {
18
- entries.set(uid, makeEntry(uid, instance, currentRoute));
35
+ if (HIDE_INTERNALS && isInternalInstance(instance)) {
36
+ return null;
37
+ }
38
+ const parentUid = HIDE_INTERNALS ? nearestTrackedAncestorUid(instance) : instance.$parent?.$.uid;
39
+ entries.set(uid, makeEntry(uid, instance, currentRoute, parentUid));
19
40
  markDirty();
20
41
  }
21
42
  return entries.get(uid);
@@ -111,6 +132,9 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
111
132
  },
112
133
  mounted() {
113
134
  const entry = ensureEntry(this);
135
+ if (!entry) {
136
+ return;
137
+ }
114
138
  entry.mountCount++;
115
139
  const isHydration = options.isHydrating?.() ?? false;
116
140
  if (isHydration && entry.mountCount === 1) {
@@ -133,6 +157,9 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
133
157
  },
134
158
  renderTriggered({ key, type }) {
135
159
  const entry = ensureEntry(this);
160
+ if (!entry) {
161
+ return;
162
+ }
136
163
  entry.triggers.push({ key: String(key), type, timestamp: performance.now() });
137
164
  pendingTriggeredRenders.add(entry.uid);
138
165
  if (entry.triggers.length > 50) {
@@ -142,6 +169,9 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
142
169
  },
143
170
  updated() {
144
171
  const entry = ensureEntry(this);
172
+ if (!entry) {
173
+ return;
174
+ }
145
175
  pendingTriggeredRenders.delete(entry.uid);
146
176
  entry.rerenders++;
147
177
  markRectDirty(entry.uid);
@@ -213,7 +243,7 @@ export function setupRenderRegistry(nuxtApp, options = {}) {
213
243
  }
214
244
  return { getAll, getSnapshot, snapshot, reset, setRoute };
215
245
  }
216
- function makeEntry(uid, instance, route) {
246
+ function makeEntry(uid, instance, route, parentUid) {
217
247
  const type = instance.$.type;
218
248
  const parentType = instance.$parent?.$?.type;
219
249
  const element = describeElement(instance.$el);
@@ -231,7 +261,7 @@ function makeEntry(uid, instance, route) {
231
261
  avgMs: 0,
232
262
  triggers: [],
233
263
  timeline: [],
234
- parentUid: instance.$parent?.$.uid,
264
+ parentUid,
235
265
  isPersistent: false,
236
266
  isHydrationMount: false,
237
267
  route
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-devtools-observatory",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "description": "Nuxt DevTools: useFetch Dashboard, provide/inject Graph, Composable Tracker, Render Heatmap",
5
5
  "license": "MIT",
6
6
  "repository": {