bugsnag/bugsnag-laravel
Bugsnag's Laravel and Lumen notifier automatically reports unhandled exceptions and crashes, lets you report handled exceptions via Bugsnag::notify(), and attaches user info and custom diagnostic data to show how many people are affected.
Install and wire up (5 minutes)
composer require bugsnag/bugsnag-laravel
php artisan vendor:publish --provider="Bugsnag\BugsnagLaravel\BugsnagServiceProvider" --tag=config
Laravel’s package auto-discovery registers the service provider and the Bugsnag facade alias. For Lumen you must wire it manually:
$app->register(Bugsnag\BugsnagLaravel\BugsnagServiceProvider::class);
$app->configure('bugsnag');
class_alias(Bugsnag\BugsnagLaravel\Facades\Bugsnag::class, 'Bugsnag');
Set the key in .env rather than editing config/bugsnag.php:
BUGSNAG_API_KEY=your-key-here
BUGSNAG_RELEASE_STAGE=production
Where to look first
config/bugsnag.php — the only file you’ll routinely touch (api_key, release_stage, notify_release_stages, discard_classes, redacted_keys, max_breadcrumbs, attach_hidden_context).Bugsnag facade — the day-to-day surface: notify(), notifyException(), notifyError(), leaveBreadcrumb(), setUser(), and the metadata/feature-flag APIs.docs.bugsnag.com for configuration depth.First use case: nothing
The most valuable first step is confirming unhandled exceptions already arrive without any code change. Trigger a 500 in a staging environment, verify the report in the dashboard, then check that release_stage shows up correctly. Only after that should you start adding explicit reporting calls.
Second use case: reporting something you caught
try {
$this->chargeCard($order);
} catch (PaymentGatewayException $e) {
Bugsnag::notifyException($e, function (Report $report) use ($order) {
$report->setSeverity('warning');
$report->addMetadata('order', ['id' => $order->id, 'total' => $order->total]);
});
// then handle gracefully — Bugsnag does not swallow the exception
}
Identify the user once, in middleware
The highest-leverage integration is a small middleware that calls Bugsnag::setUser() so every subsequent report (handled or not) carries it:
public function handle($request, Closure $next)
{
if ($user = $request->user()) {
Bugsnag::setUser((string) $user->id, $user->email, $user->name);
}
return $next($request);
}
Pass primitives (id/email/name), not the Eloquent model. Historic bugs in this package fetched lazy relationships while populating user data (fixed in 2.7.1), and passing scalars keeps payloads small and avoids accidental serialization of sensitive columns.
Breadcrumbs tell the story
Automatic breadcrumbs (queries, logs, events, queue context) cover most cases, but manual crumbs around irreversible or confusing operations are worth the line:
Bugsnag::leaveBreadcrumb('Applying discount', Bugsnag\Breadcrumbs\Breadcrumb::INFO, [
'code' => $code,
]);
Bound the volume with max_breadcrumbs in config (added in 2.25.0) rather than leaving the default.
Enrich globally with beforeNotify-style callbacks
Instead of repeating metadata at every call site, register a callback once in a service provider:
Bugsnag::registerCallback(function (Report $report) {
$report->addMetadata('app', [
'tenant' => tenant()->id ?? null,
'commit' => config('app.commit'),
]);
});
Be careful: global callbacks run for every report including ones already enriched — keep them cheap and idempotent.
Configure discardClasses for known noise
discardClasses (2.22.0) matches exception class names or PHP error names and drops them before sending. This is the correct tool for Symfony\Component\HttpKernel\Exception\NotFoundHttpException, bot-driven InvalidArgumentExceptions, and similar categories that would otherwise burn your event quota. Do it in config, not with if statements scattered across controllers.
Redaction: redactedKeys, not filters
filters is deprecated (2.22.0). redactedKeys supports exact case-insensitive strings and regexes for partial/wildcard matching:
'redacted_keys' => ['password', 'authorization', '/.*_token$/i'],
Note that since 2.20.1 the default filters value is null so Bugsnag PHP’s own defaults apply — don’t “restore” ['password'] and accidentally lose the newer built-in keys.
Logging channel and PSR-3
The package ships a Monolog handler, so you can add a bugsnag channel and Log::channel('bugsnag')->error(...). This is the right integration point for code that already uses a logger rather than the facade. Watch out for multi-logger setups (the duplicate-event bug fixed in 2.15.2) and for logging the same exception both via report() and via the channel.
Queues and commands
Queue context and job info are recorded automatically (2.3.0); console command metadata since 2.9.0. For jobs, use failed() plus Bugsnag::notifyException($e) when you need custom metadata — but remember $this->fail($e) already reports via the exception handler in most setups, so verify you aren’t double-reporting.
Deploy tracking
php artisan bugsnag:deploy --repository=https://github.com/acme/app --revision=$GIT_SHA
Wire this into your deploy pipeline as the last step, otherwise release-stage filtering and regression detection are effectively blind.
Feature flags / experiments
2.24.0 added the feature-flag APIs — call them in your flag-resolution service so reports can be sliced by variant. This is where the package earns its keep over generic error tracking: debugging “broken only for the 10% on the new checkout flow” becomes trivial.
Out-of-memory reporting
Register OomBootstrapper in app/Http/Kernel.php (2.22.0) — it bumps the memory limit by 5 MiB so a fatal OOM can still be serialized and sent.
BUGSNAG_API_KEY, notify_release_stages, and that release_stage matches what you filtered on — in that order.config:cache traps. Config values read from env() inside a cached config are frozen at build time. Keep all environment variation in .env-driven keys in config/bugsnag.php, and re-run config:cache after changing them.notify() does not rethrow. It reports and returns. For flow control you still need throw or explicit handling — a common source of “we report it but the request succeeds” confusion.session() isn’t available; don’t chase a config flag that won’t help. Attach the relevant state as metadata instead./var/www/... instead of relative paths, use the project_root / strip_path regex options (2.19.0) — this matters for source-map/grouping quality and is the most common “why is grouping weird” cause.attach_hidden_context (2.28.0). Laravel 11’s Context is attached as Laravel Context; hidden context is opt-in. Enabling it can pull in secrets — pair it with redacted_keys before turning it on in production.discardClasses matches class names exactly. No substring matching. Subclass filtering requires listing each class or handling it upstream.$dontReport or to discard_classes. This is the single biggest quota saver in a typical app.bugsnag.guzzle alias in the container (2.21.0) rather than forking the notifier.composer.json; when upgrading Laravel, upgrade this package first — most “it stopped reporting after the framework upgrade” issues are a resolved-version mismatch, not a bug.How can I help you explore Laravel packages today?