goodport / laravel-js-error-logger
Capture browser JavaScript errors and write them to a Laravel log channel.
Package info
github.com/goodport/laravel-js-error-logger
pkg:composer/goodport/laravel-js-error-logger
Requires
- php: ^8.2
- ext-mbstring: *
- laravel/framework: ^12.69
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.5.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 08:38:36 UTC
README
Capture browser JavaScript errors and write them to a dedicated Laravel log channel.
The package ships a small inline beacon, a stateless POST endpoint, validation, deduplication, throttling, and automatic log channel registration. It works without publishing configuration, without a session, and without changes to the application CSRF setup.
What it captures
- Uncaught JavaScript errors.
- Unhandled promise rejections.
- Failed
<script>and<link>resource loads. - Vite chunk preload failures.
The beacon uses navigator.sendBeacon and is written in ES5, so it does not need a
build step and does not depend on application bundles.
Requirements
- PHP 8.2 or newer.
- Laravel 12.69 or newer. Composer blocks older Laravel releases because of active security advisories.
- The
mbstringPHP extension. - A browser with
navigator.sendBeacon. Older browsers silently skip reporting.
Installation
composer require goodport/laravel-js-error-logger
The service provider is discovered automatically. The log channel is registered when the package boots; no migration or manual logging configuration is required.
Place the beacon in the document <head> before application bundles, so early errors
are captured:
<head> <x-js-error-logger::beacon /> </head>
The component renders only when enabled is true and the current route is not
excluded (see Route exclusions).
When the application uses a Content-Security-Policy, pass a per-response nonce:
<x-js-error-logger::beacon :nonce="$cspNonce" />
If the attribute is omitted, the component uses Vite::cspNonce() when it is
available.
Publishing
Publish the configuration or the views only when you need to change them:
php artisan vendor:publish --tag=js-error-logger-config php artisan vendor:publish --tag=js-error-logger-views
The template can also be overridden at:
resources/views/vendor/js-error-logger/components/beacon.blade.php
Configuration
// config/js-error-logger.php return [ 'enabled' => env('JS_ERROR_LOGGER_ENABLED', true), 'route' => [ 'path' => env('JS_ERROR_LOGGER_PATH', 'js-error-logger'), 'name' => 'js-error-logger.report', 'middleware' => [], ], 'throttle' => [ 'per_minute' => env('JS_ERROR_LOGGER_THROTTLE', 20), ], 'log' => [ 'channel' => env('JS_ERROR_LOGGER_CHANNEL', 'js-error-logger'), 'message_prefix' => 'Frontend JavaScript error: ', ], 'limits' => [ 'max_events_per_page' => 5, 'max_message_length' => 300, 'max_url_length' => 2048, ], 'dedupe' => [ 'enabled' => true, 'store' => null, 'minutes' => 30, ], 'privacy' => [ 'strip_query_strings' => true, 'include_ip' => true, 'include_user_agent' => true, ], 'ignore' => [ 'user_agents' => ['bot', 'spider', 'crawler', 'slurp'], 'messages' => [ 'request aborted', 'user aborted a request', 'was aborted', 'aborted without reason', ], 'routes' => [], ], 'capture' => [ 'resource_errors' => true, 'preload_errors' => true, ], ];
| Key | Default | Description |
|---|---|---|
enabled |
true |
Enables the beacon, the endpoint, and error writing. |
route.path |
js-error-logger |
Report endpoint path. Leading and trailing slashes are removed. |
route.name |
js-error-logger.report |
Report endpoint route name. |
route.middleware |
[] |
Extra route middleware. web, session, auth, and auth:* are rejected. |
throttle.per_minute |
20 |
Per-IP request limit for the endpoint. |
log.channel |
js-error-logger |
Log channel used for reported errors. |
log.message_prefix |
Frontend JavaScript error: |
Prefix added to the logged message. |
limits.max_events_per_page |
5 |
Maximum beacon events sent from one page load. |
limits.max_message_length |
300 |
Maximum accepted and logged message length. |
limits.max_url_length |
2048 |
Maximum accepted and logged URL or file length. |
dedupe.enabled |
true |
Enables server-side deduplication. |
dedupe.store |
null |
Cache store name. null uses the application default store. |
dedupe.minutes |
30 |
Deduplication window in minutes. |
privacy.strip_query_strings |
true |
Strips query strings and fragments from beacon URLs, resource sources, stack files, and logged data. Set to false to preserve them on both the client and the server. |
privacy.include_ip |
true |
Adds the remote IP to the log context. |
privacy.include_user_agent |
true |
Adds the User-Agent to the log context. |
ignore.user_agents |
bot list | Case-insensitive User-Agent fragments that are ignored. |
ignore.messages |
abort list | Case-insensitive message fragments that are ignored. |
ignore.routes |
[] |
Route names or * patterns where the beacon is not rendered. |
capture.resource_errors |
true |
Captures failed script and stylesheet loads. |
capture.preload_errors |
true |
Captures vite:preloadError events. |
The package recursively merges these defaults with the application configuration. A partially published config therefore keeps nested defaults that it does not override.
Invalid configuration fails fast with
Goodport\JsErrorLogger\Config\InvalidConfigurationException. This includes empty
route paths and names, empty log channels, non-positive numeric limits, and
web, session, auth, or auth:* route middleware.
Route exclusions
Add route names or * patterns to ignore.routes to skip rendering the beacon on
those pages. Matching runs at render time against the current named route, so it
uses the route name the application actually resolved. Patterns follow the
Str::is() wildcard syntax and are case-sensitive.
'ignore' => [ 'routes' => ['checkout.*', 'account.settings'], ],
Unnamed routes and error pages such as 404 have no route name and cannot be
excluded this way. In SPA and Inertia applications, client-side navigations do not
re-render the Blade component, so ignore.routes only applies to the route of the
initial full-page load.
How it works
- The Blade component renders an inline ES5 beacon unless reporting is disabled or
the current named route matches
ignore.routes. The endpoint comes from the named route and the client limits come from the package configuration. - The beacon posts a JSON
Blobwith{url, message, file, line}. - The endpoint runs
js-error-logger.enabled, then the named rate limiter, then any configured route middleware. It is not placed in thewebmiddleware group, so no session or CSRF token is involved. - When
enabledisfalse, the middleware returns204before request validation, including for invalid payloads. ReportRequestvalidates the payload. Validation failures always return a bodiless422, becausesendBeaconcannot set anAcceptheader.ReportErrorsanitizes the payload, applies the ignore lists, reserves a deduplication key, and writes to the configured channel.- The endpoint returns
204. A cache or logger failure never breaks this response.
The client also rejects Script error., foreign-origin sources for window errors,
and repeated fingerprints within the same page. Client-side events are capped by
limits.max_events_per_page.
Deduplication
The server builds a fingerprint from the message, file, and line. The page URL is
not included, so the same error on different pages is written once. The fingerprint
key is stored with Cache::add() for dedupe.minutes.
Deduplication reduces noise; it is not an abuse protection. The per-IP throttle is the request limiter.
If the cache store fails, the error is still written and the cache failure is sent
to the application default log. If the configured log channel fails, the endpoint
still returns 204, and the failure is reported once per process to the default log.
Log channel
The channel name comes from log.channel, which defaults to js-error-logger.
When logging.channels.<log.channel> is not declared, the package registers a
daily channel under that name:
- path:
storage/logs/js-errors.log; - level:
error; - retention: 30 days;
- formatter: JSON.
An application-declared channel is never overwritten. Set log.channel to an
existing channel such as stack to use it instead.
A nonexistent channel is handled by Laravel's own log manager, which falls back to the emergency logger. The package does not hide that framework behavior.
Responses
| Situation | Response |
|---|---|
| Accepted event | 204 No Content |
| Reporting disabled | 204 No Content, before validation |
| Invalid payload | 422 with an empty body |
| Rate limit exceeded | 429 produced by Laravel |
| Logger or cache failure | 204 No Content |
The 429 response is generated by the framework and can contain a body and
headers.
Privacy and security
- The endpoint is public by design. Message length, URL and file length, page event count, and request rate are limited.
- All C0 and C1 control characters,
\x08,\t,\r,\n,\0, U+2028, and U+2029 are replaced before logging. - Message, URL, and file lengths are bounded; client truncation is surrogate-safe and server truncation is multibyte-safe.
- Query strings and fragments are removed from beacon URLs, resource sources, stack
files, and logged data by default. Set
privacy.strip_query_stringstofalseto preserve them on both the client and the server. - IP and User-Agent can be excluded from the log context.
- Payload content is written to the log only; it is never rendered as HTML or executed.
- The component escapes the nonce attribute.
The browser still sends same-origin cookies because that is normal browser behavior. The package does not start a session and does not use cookie data.
Limitations
- Cross-origin errors without CORS are reported by the browser as
Script error.and are intentionally skipped. - Stacks are not sent. Promise rejections include only the first stack line as the file value.
- No user, request, or page context is sent beyond the page URL.
- Deduplication and the rate limiter both use the application cache store. The
cache must be shared between application processes; the
arraystore disables deduplication and cross-request throttling in production. X-Forwarded-Foris trusted only as far as the application'sTrustProxiesconfiguration trusts it.- Reporting is best effort: the endpoint never reports an error caused by the diagnostic request itself.
Route cache
The route is registered through loadRoutesFrom, so it remains compatible with
php artisan route:cache. The path, name, and middleware are fixed when the route
cache is built. After changing route.path, route.name, or route.middleware,
run:
php artisan route:clear php artisan route:cache
Development
composer install
composer test
composer format
The test suite uses Orchestra Testbench. It does not need a database, migrations, network access, or a browser.
License
MIT. See LICENSE.