nelmio/js-logger-bundle
NelmioJsLoggerBundle integrates JavaScript error logging into Symfony applications, capturing browser-side errors and warnings and sending them to your server logs via Monolog. It helps centralize frontend issues alongside backend logs for easier debugging.
Before anything else: this is a Symfony bundle, not a Laravel package. There is no JsLoggerServiceProvider, no auto-discovery, no config/js-logger.php. If your stack is pure Laravel, you cannot composer require and php artisan your way to a working install — you will use it either (a) inside a Symfony app that shares your architecture, or (b) as a reference implementation to copy the pattern into Laravel. Both are legitimate; decide which one you are doing before writing code.
If you actually have a Symfony component in your stack:
composer require nelmio/js-logger-bundleNelmio\JsLoggerBundle\NelmioJsLoggerBundle in config/bundles.phpconfig/packages/nelmio_js_logger.yaml with at minimum log_level and the route/controller you want exposedconfig/routes/nelmio_js_logger.yaml)@NelmioJsLogger/logger.js), and confirm the browser is actually POSTing by watching var/log/dev.logFirst use case (the only one worth doing first): capture uncaught window errors and unhandled promise rejections from production browsers into a dedicated Monolog channel. Nothing else. Do not wire up console.error interception, breadcrumbs, or source maps on day one.
If you are porting to Laravel (the more likely scenario for this audience): read Controller/LogController.php and the Resources/public/js/ shim first — they are ~100 lines combined. Then build:
POST /_js-errors route → a thin controller that validates and writes to Log::channel('javascript')@sentry/browser-style shim) that hooks window.addEventListener('error') and 'unhandledrejection', builds a JSON payload, and fires it with navigator.sendBeaconjavascript log channel in config/logging.php pointing at a separate daily file so error noise never pollutes application logsEndpoint contract. Mirror the bundle's shape rather than inventing one: { message, url, line, column, error (stack), userAgent }. Keep it a flat JSON object — the bundle's JS does no nesting, and flat payloads are trivially indexable if you ever ship them to Elastic/Datadog.
Dedicated channel, always. The single most important pattern. Route JS errors to js_errors / javascript channel, separate from stack and single. You will want to tail it independently, set its own retention, and alert on it without alerting on app errors.
Sampling at the edge. Real browsers generate floods (extension errors, third-party script failures, ad blockers). Wrap the client shim in a sample rate:
if (Math.random() > 0.1) return; // 10% of errors
Better still, dedupe client-side by hashing message + url + line and only sending the first occurrence per page load.
Use sendBeacon, not fetch. The bundle targets old browsers, but for new code navigator.sendBeacon(url, JSON.stringify(payload)) survives page unload and doesn't require awaiting a promise, which is exactly what you need when the error fires mid-navigation. Fall back to fetch(..., { keepalive: true }) if the payload needs custom headers.
Context enrichment. The bundle's logged_content config is the analogue of "what do I attach to each entry." In Laravel, put this in middleware or the controller:
user_id from Auth::id() (never the session cookie, never the full user agent hash)request_id from a correlation middleware so a JS error can be joined to a server-side log lineroute / url from $_SERVER['REQUEST_URI'] sent by the clientapp_version — commit SHA or git tag; without this you cannot tell whether a fix shippedIntegration tip — throttle at the route level, not in the controller. Route::post('/_js-errors', ...)->middleware('throttle:60,1'). The bundle exposes no built-in rate limiting; if you port it, add this deliberately. A misbehaving script in one user's tab can otherwise generate thousands of log lines per minute.
Exclusion patterns. The bundle's exclude_patterns (regex on the source URL) is a pattern worth copying: never log errors originating from /admin, /horizon, or third-party domains you don't own. Filtering server-side is cheap; filtering downstream in Kibana is not.
Stack traces, not messages. A JS message alone ("Cannot read property 'x' of undefined") is worthless. Ensure the payload's error field contains the full Error.stack. If minified, note the version so you can map it later.
You cannot use this with Laravel out of the box. There is no Laravel bridge, no facade, no config publish. composer require nelmio/js-logger-bundle in a Laravel app installs files that Laravel never loads. Treat it as documentation, not a dependency. This is the single largest source of wasted time for a Laravel developer picking this up.
The endpoint is unauthenticated by design. That means anyone can POST arbitrary log entries to your server — log injection, storage exhaustion, PII fabrication in your incident data. Mitigations: throttle hard, cap message and error length (e.g. 4KB), reject payloads where url origin doesn't match app.url, and never eval or render a logged value. If you need auth, add it in your port — the bundle won't do it for you.
CSP will silently block the beacon. If you ship Content-Security-Policy, add your logging origin to connect-src. Symptom: no errors in your log channel, browser devtools console shows a Refused to connect violation. This is the most common "why is nothing logging" complaint.
sendBeacon sends text/plain by default. If your controller requires application/json, either accept text/plain and json_decode manually, or use a Blob with an explicit type: new Blob([body], { type: 'application/json' }). This will bite you during local testing even though it works in production.
Beware the error loop. If your logging endpoint itself throws an uncaught error, the client shim catches it and POSTs again — infinite loop, log flood. Always return 204 No Content unconditionally from the endpoint and swallow all exceptions in the handler. Wrap in try { ... } catch { return response()->noContent(); }.
Do not log from a logging channel that writes to the same channel being read. Sounds obvious, but if you point the JS channel at stack and also have middleware logging request failures there, a bad deploy will produce a self-referential flood. Keep javascript on its own file.
Extension points (if you're on Symfony). The bundle is small enough to extend cleanly: override the controller via a service alias, swap the JS template with your own Twig template, and set log_level per environment. In a Laravel port, the equivalents are: replace the controller outright, publish the Blade partial, and set LOG_LEVEL in .env.
Version pinning and maintenance signal. Last release 2026-04-08 with 176 stars and 0 dependents is a warning sign — this is a small, low-traffic package. For a Laravel team, that's one more reason to port the pattern (~100 lines) rather than depend on a bundle that ships on a Symfony release cadence disconnected from yours.
Debugging checklist when nothing appears in logs: (1) browser connect-src CSP, (2) ad blocker intercepting the route name (/log, /error are commonly blocked — use an innocuous path like /_js-errors), (3) the route actually registered (php artisan route:list | grep js-errors), (4) sendBeacon payload content-type, (5) log channel permissions (storage/logs writability), (6) sampling filter dropping everything.
How can I help you explore Laravel packages today?