Repository navigation
[Debugging 1/4] Capture browser errors and page views from site pages for the dashboard and MCP #2089
Description
Part of #2088 · step 1 of 4
Why
JavaScript errors thrown on a page lerd serves only ever reach that browser's own console. lerd captures the PHP half of a request in detail, but the frontend half is invisible to it, and an AI assistant debugging a site over MCP cannot see why a page broke in the browser without asking the developer to copy the console. The later steps build on this one: request linking (#2090) joins these events to the PHP request behind them, and the debug bar (#2092) shows them on the page.
Plan
Capture
Browser capture sits next to debug capture with a switch of its own, off by default since it changes the HTML of every site it covers, toggled from the Sites header, System Health, lerd browser-capture on|off and MCP. While it is on, nginx injects a script before </head> on PHP-FPM, host-proxy and custom-container sites through sub_filter, so it works the same for every framework and needs nothing from the app; proxy vhosts ask the backend for an uncompressed body so the page can be rewritten.
The script reports through navigator.sendBeacon to a same-origin route that nginx proxies to lerd-ui, naming the site in headers nginx sets itself, and lerd-ui only accepts what nginx handed over, so a page cannot pose as another site and a client on the LAN cannot post to it directly. Reports land in the same in-memory buffer as the debug lenses.
What is reported
Uncaught errors and unhandled rejections are always reported. Per site, these can be turned on:
| Setting | Default |
|---|---|
console.error |
on |
console.warn |
off |
| Failed requests: 4xx, 5xx, no response (flagged when cross-origin) | off |
| Resources that failed to load | off |
| Page views, SPA navigation included | on |
| DOM events the site lists | none |
Each page view starts its own group, so an error a single-page app throws after navigating sits under the page it happened on. DOM events are listed by name with an optional dot path into the event for the message; only names and paths are configurable, never code, since the script runs on every page. A value that is an object is described by what identifies it, an error by its message, a response by its status and URL, a DOM node by its tag and id, a cycle as [Circular], kept short.
Settings
Settings live under a new optional browser_capture key in .lerd.yaml, and in the site registry when the project has no .lerd.yaml, so changing one from the dashboard never creates the file. The script is served per site with its settings baked in, so a change takes effect on the next page load without an nginx reload; only the global switch and a changed route rewrite vhosts. The route defaults to /_lerd/browser and can be moved for an app that serves /_lerd itself.
Presets
The framework store publishes presets for libraries that report failures as DOM events, starting with Inertia.js, Hotwire Turbo, htmx and Vite, each with the composer and npm packages that show a project uses it. The site's Browser settings offer the detected ones with one click; adding a preset copies its events into the site's own list and records it under presets, and removing it keeps any event another added preset still declares. The presets themselves go to lerd-env/frameworks in a pull request of their own.
Dashboard and MCP
A new Browser lens in the site's Debug tab and in the global Debug view shows events grouped per page view, filterable by type, with a settings modal per site. A site without PHP gets a Debug tab holding only this lens. Over MCP, diag gains browser_events, which returns page views newest first with what broke on each, counts per type and a hint that explains an empty answer, plus browser_toggle and browser_presets for setting it up and reading which presets were detected.
Limits
FrankenPHP sites and the worktrees of host-proxy sites are not covered yet, a strict nonce-based CSP blocks the injected script, and minified production bundles without source maps give stacks that are hard to read. Every browser event already carries an id for the page view it belongs to, which is what request linking will join to the request id the devtools extension stamps on the PHP side.
Activity
[-]Capture browser errors and page views from site pages for the dashboard and MCP[/-][+][Debugging 1/5] Capture browser errors and page views from site pages for the dashboard and MCP[/+][-][Debugging 1/5] Capture browser errors and page views from site pages for the dashboard and MCP[/-][+][Debugging 1/4] Capture browser errors and page views from site pages for the dashboard and MCP[/+]ExeQue commentedon Oct 5, 2026
Folded into #2088, which now covers the whole series in one PR, #2113.