You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
analog: fixes and improvements for the Analog inspector #352
Fixes and improvements from a code audit of the analog inspector, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
Problems
Medium
Server calls mix across tabs: the call log is a module-level calls[] and AnalogCall has no pageId, so calls, duplicate loads, refetches and their lint findings from several tabs end up in one list. pangular:analog-server-calls has no page argument. Tag browser requests with the page id (a header set by the overlay, or a Referer-to-page map), store it on AnalogCall, add a page filter to the tool and the Server tab, and run duplicate and refetch detection per page (packages/devtools/src/analog-server-log.ts:40-89, packages/devtools/src/rpc/analog-register.ts:312-327).
Lint reads the newest page, not the shown one:analogLint uses state.pages[0], while the panel picks its page with hostPageId but calls analog-lint without it. A panel docked in tab A can show tab B's hydration errors and restart-needed findings. Add an optional pageId to analogLint, the analog-lint RPC and pangular:analog-lint, and pass hostPageId from the panel (packages/devtools/src/rpc/analog-tools.ts:665, app/src/pages/analog-inspector.ts:2273, app/src/pages/analog-inspector.ts:2426).
Synchronous rescans on every push:project() is cached for only 2 s (SCAN_CACHE_MS) and runs on every push-analog and every panel refresh, which fires 300 ms after any state change. Each scan walks up to 4000 files with readdirSync and readFileSync and blocks the Vite event loop. Invalidate the cache from Vite's watcher (src/app/pages, src/app/routes, src/server, src/content, vite.config) instead of a TTL, and have the panel refetch lint and render only when calls or pages change (packages/devtools/src/rpc/analog-register.ts:45, packages/devtools/src/rpc/analog-register.ts:197-215, app/src/pages/analog-inspector.ts:2379-2383).
Server functions with type arguments are missed: the regex =\s*(\w+)\s*\( doesn't match export const getX = serverFn<Input>(...), and re-exports are skipped too. Those functions drop out of the list and call names, and the server-without-load lint can fire falsely. Allow (?:<[^>]*>)? before the paren, or parse with ts.createSourceFile (packages/devtools/src/rpc/analog-scan.ts:411-413).
Config parsing stops after 4000 characters:analogConfig reads source.slice(start, start + 4000), so routeRules, prerender.routes or apiPrefix past a large nitro block are silently ignored. The lazy prerender regex can also pick up a routes: key outside the prerender block. Slice to the matching close paren with depth counting, read routes only inside prerender: {...}, and report when the config couldn't be fully parsed (packages/devtools/src/rpc/analog-scan.ts:560, packages/devtools/src/rpc/analog-scan.ts:578-580).
Low
Server files only looked up in src/app/pages:serverFiles walks src/app/pages only, while routeFiles() also walks app/routes and src/app/routes. .server.ts files beside routes there are missed by orphan-server-file and the existence check in resolveAnalogReport. Walk the same roots as routeFiles() (packages/devtools/src/rpc/analog-scan.ts:691) (to confirm).
load() payload stringified on every tick:loadSummary runs JSON.stringify twice (redacted preview and byte count) on every push, even when the data hasn't changed. Cache the summary in a WeakMap keyed by the data object (packages/devtools/src/analog-runtime.ts:152-166, packages/devtools/src/analog-runtime.ts:299-307).
Report strings have no length limit:isAnalogReport checks types and counts only, and redactAnalogReport clips hydrationErrors only after redaction. url, load.preview, configPaths entries and chain paths can be any size and are run through regex redaction and copied into shared state. Cap them in isAnalogReport or clip before redaction (url 2000, preview 1000, path 500) (packages/devtools/src/rpc/analog-tools.ts:52-82, packages/devtools/src/analog-redact.ts:10-23).
Analog RPCs missing from RPC_INSPECTOR: no push-analog, forget-analog-page or analog-* entries. It works only because registerAnalog runs behind on.analog and agent tools fall back on the analog- prefix. Add push-analog, forget-analog-page, analog-clear-calls, analog-project, analog-explain-url, analog-lint, analog-render, analog-text and analog-call-api (packages/devtools/src/config.ts:430, packages/devtools/src/config.ts:611).
list-pages leaves out Analog pages:summarizePages gets components, signals, injectors, NgRx, forms, router, pipes and http, but not analog, so the ids the page argument of analog-current-page needs aren't discoverable from Analog reports. Pass the analog pages in (packages/devtools/src/devframe.ts:2912-2925).
Lint badge counts info findings as warnings:const errors = this.findings().length counts every finding and tones it warn, so the info-only api-outside-prefix makes the tab look broken. Count errors and warnings apart, use the bad tone for errors, and leave info out (app/src/pages/analog-inspector.ts:2359, app/src/pages/analog-inspector.ts:2370).
Shadowed-page link only matches .page.ts:shadowed() pulls the file from the message with /(\/\S+\.page\.ts)/, so .page.analog and .page.ag pages are never linked. Add the page file as a structured field on the content-shadows-page finding instead of parsing the message (app/src/pages/analog-inspector.ts:2518-2523, packages/devtools/src/rpc/analog-scan.ts:1020-1028).
Page chain follows firstChild only: files rendered in named or auxiliary outlets never show in the current page chain. Walk every child snapshot and tag each entry with its outlet (packages/devtools/src/analog-runtime.ts:103-121).
Load endpoint URLs aren't encoded:loadEndpointUrl fills params without encodeURIComponent, and the catch-all falls back to the last param value, so explained endpoints are wrong for values with reserved characters or routes with several params plus a catch-all. Encode each value and map the catch-all by its own param name (packages/devtools/src/rpc/analog-tools.ts:137-150).
Improvements
Server route playground: fill :params from the route pattern, edit query, headers and cookies, keep request history per route, show response headers and timing, and copy as curl or fetch. Today the sender takes method, path and a JSON body only (app/src/pages/analog-inspector.ts, packages/devtools/src/rpc/analog-register.ts).
Payload tab: list each TransferState entry with its load() or server function name (nameServerFns already exists), size and whether the browser reused it, and link duplicate-load findings to the key (packages/devtools/src/analog-runtime.ts, packages/devtools/src/rpc/analog-tools.ts, app/src/pages/analog-inspector.ts).
Runtime config view: show import.meta.envVITE_/ANALOG_ keys, Nitro runtimeConfig and the resolved analog() options, redacted with redaction.secretNames (packages/devtools/src/rpc/analog-scan.ts, packages/devtools/src/rpc/analog-register.ts).
Nitro internals: list Nitro plugins, tasks (run action in ACTION_TOOLS), storage mounts and cached-route entries behind isr/swr rules, with a purge action (packages/devtools/src/rpc/analog-register.ts, packages/devtools/src/config.ts).
Per-route data inspector: show the full redacted load() value as a lazy tree on demand through an RPC to the page, not just the 1000-character preview (packages/devtools/src/analog-runtime.ts, app/src/pages/analog-inspector.ts).
Render mode check: fetch a page with Accept: text/html from the hub and compare cache-control, x-nitro-prerender and x-analog-no-ssr with the configured mode (packages/devtools/src/rpc/analog-tools.ts, app/src/pages/analog-inspector.ts).
Content tab: a frontmatter schema check driven by a config option, contentFilesSource/injectContentFiles usage, and a rendered markdown preview (packages/devtools/src/rpc/analog-scan.ts, app/src/pages/analog-inspector.ts).
Link a page file to its Vite transform stack so the .analog/.ag compile and the serverFn id rewrite can be seen (packages/devtools/src/vite.ts, app/src/pages/analog-inspector.ts).
Build analysis: after analog build, show per-route chunk sizes from dist/analog and which pages pull which lazy chunks, next to the prerender plan (packages/devtools/src/rpc/analog-scan.ts, app/src/pages/analog-inspector.ts).
Context
Fixes and improvements from a code audit of the analog inspector, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.
Problems
Medium
calls[]andAnalogCallhas nopageId, so calls, duplicate loads, refetches and their lint findings from several tabs end up in one list.pangular:analog-server-callshas nopageargument. Tag browser requests with the page id (a header set by the overlay, or a Referer-to-page map), store it onAnalogCall, add a page filter to the tool and the Server tab, and run duplicate and refetch detection per page (packages/devtools/src/analog-server-log.ts:40-89,packages/devtools/src/rpc/analog-register.ts:312-327).analogLintusesstate.pages[0], while the panel picks its page withhostPageIdbut callsanalog-lintwithout it. A panel docked in tab A can show tab B's hydration errors and restart-needed findings. Add an optionalpageIdtoanalogLint, theanalog-lintRPC andpangular:analog-lint, and passhostPageIdfrom the panel (packages/devtools/src/rpc/analog-tools.ts:665,app/src/pages/analog-inspector.ts:2273,app/src/pages/analog-inspector.ts:2426).project()is cached for only 2 s (SCAN_CACHE_MS) and runs on everypush-analogand every panel refresh, which fires 300 ms after any state change. Each scan walks up to 4000 files withreaddirSyncandreadFileSyncand blocks the Vite event loop. Invalidate the cache from Vite's watcher (src/app/pages,src/app/routes,src/server,src/content,vite.config) instead of a TTL, and have the panel refetch lint and render only when calls or pages change (packages/devtools/src/rpc/analog-register.ts:45,packages/devtools/src/rpc/analog-register.ts:197-215,app/src/pages/analog-inspector.ts:2379-2383).=\s*(\w+)\s*\(doesn't matchexport const getX = serverFn<Input>(...), and re-exports are skipped too. Those functions drop out of the list and call names, and the server-without-load lint can fire falsely. Allow(?:<[^>]*>)?before the paren, or parse withts.createSourceFile(packages/devtools/src/rpc/analog-scan.ts:411-413).analogConfigreadssource.slice(start, start + 4000), sorouteRules,prerender.routesorapiPrefixpast a largenitroblock are silently ignored. The lazy prerender regex can also pick up aroutes:key outside theprerenderblock. Slice to the matching close paren with depth counting, readroutesonly insideprerender: {...}, and report when the config couldn't be fully parsed (packages/devtools/src/rpc/analog-scan.ts:560,packages/devtools/src/rpc/analog-scan.ts:578-580).Low
src/app/pages:serverFileswalkssrc/app/pagesonly, whilerouteFiles()also walksapp/routesandsrc/app/routes..server.tsfiles beside routes there are missed byorphan-server-fileand the existence check inresolveAnalogReport. Walk the same roots asrouteFiles()(packages/devtools/src/rpc/analog-scan.ts:691) (to confirm).loadSummaryrunsJSON.stringifytwice (redacted preview and byte count) on every push, even when the data hasn't changed. Cache the summary in aWeakMapkeyed by the data object (packages/devtools/src/analog-runtime.ts:152-166,packages/devtools/src/analog-runtime.ts:299-307).isAnalogReportchecks types and counts only, andredactAnalogReportclipshydrationErrorsonly after redaction.url,load.preview,configPathsentries and chain paths can be any size and are run through regex redaction and copied into shared state. Cap them inisAnalogReportor clip before redaction (url 2000, preview 1000, path 500) (packages/devtools/src/rpc/analog-tools.ts:52-82,packages/devtools/src/analog-redact.ts:10-23).RPC_INSPECTOR: nopush-analog,forget-analog-pageoranalog-*entries. It works only becauseregisterAnalogruns behindon.analogand agent tools fall back on theanalog-prefix. Addpush-analog,forget-analog-page,analog-clear-calls,analog-project,analog-explain-url,analog-lint,analog-render,analog-textandanalog-call-api(packages/devtools/src/config.ts:430,packages/devtools/src/config.ts:611).list-pagesleaves out Analog pages:summarizePagesgets components, signals, injectors, NgRx, forms, router, pipes and http, but not analog, so the ids thepageargument ofanalog-current-pageneeds aren't discoverable from Analog reports. Pass the analog pages in (packages/devtools/src/devframe.ts:2912-2925).const errors = this.findings().lengthcounts every finding and tones itwarn, so the info-onlyapi-outside-prefixmakes the tab look broken. Count errors and warnings apart, use thebadtone for errors, and leave info out (app/src/pages/analog-inspector.ts:2359,app/src/pages/analog-inspector.ts:2370)..page.ts:shadowed()pulls the file from the message with/(\/\S+\.page\.ts)/, so.page.analogand.page.agpages are never linked. Add the page file as a structured field on thecontent-shadows-pagefinding instead of parsing the message (app/src/pages/analog-inspector.ts:2518-2523,packages/devtools/src/rpc/analog-scan.ts:1020-1028).firstChildonly: files rendered in named or auxiliary outlets never show in the current page chain. Walk every child snapshot and tag each entry with its outlet (packages/devtools/src/analog-runtime.ts:103-121).loadEndpointUrlfills params withoutencodeURIComponent, and the catch-all falls back to the last param value, so explained endpoints are wrong for values with reserved characters or routes with several params plus a catch-all. Encode each value and map the catch-all by its own param name (packages/devtools/src/rpc/analog-tools.ts:137-150).Improvements
:paramsfrom the route pattern, edit query, headers and cookies, keep request history per route, show response headers and timing, and copy as curl or fetch. Today the sender takes method, path and a JSON body only (app/src/pages/analog-inspector.ts,packages/devtools/src/rpc/analog-register.ts).load()or server function name (nameServerFnsalready exists), size and whether the browser reused it, and link duplicate-load findings to the key (packages/devtools/src/analog-runtime.ts,packages/devtools/src/rpc/analog-tools.ts,app/src/pages/analog-inspector.ts).import.meta.envVITE_/ANALOG_keys, NitroruntimeConfigand the resolvedanalog()options, redacted withredaction.secretNames(packages/devtools/src/rpc/analog-scan.ts,packages/devtools/src/rpc/analog-register.ts).ACTION_TOOLS), storage mounts and cached-route entries behindisr/swrrules, with a purge action (packages/devtools/src/rpc/analog-register.ts,packages/devtools/src/config.ts).load()value as a lazy tree on demand through an RPC to the page, not just the 1000-character preview (packages/devtools/src/analog-runtime.ts,app/src/pages/analog-inspector.ts).Accept: text/htmlfrom the hub and comparecache-control,x-nitro-prerenderandx-analog-no-ssrwith the configured mode (packages/devtools/src/rpc/analog-tools.ts,app/src/pages/analog-inspector.ts).contentFilesSource/injectContentFilesusage, and a rendered markdown preview (packages/devtools/src/rpc/analog-scan.ts,app/src/pages/analog-inspector.ts)..analog/.agcompile and theserverFnid rewrite can be seen (packages/devtools/src/vite.ts,app/src/pages/analog-inspector.ts).analog build, show per-route chunk sizes fromdist/analogand which pages pull which lazy chunks, next to the prerender plan (packages/devtools/src/rpc/analog-scan.ts,app/src/pages/analog-inspector.ts).Already tracked elsewhere
analog-call-apiaccepts any dev server path, including/@fs/…and?raw: phase 1 of 4: safe by default and wired into your workflow #340.Generated by Claude Code