honeybadger-io/honeybadger-php
Honeybadger is an error and exception monitoring service for PHP applications, with first-class Laravel support via a service provider and facade. Automatically report exceptions, track deployments, and surface errors with full stack traces and context so you can find and fix bugs faster.
Minimal setup in a Laravel app:
composer require honeybadger-io/honeybadger-php
Add credentials to .env (never hardcode them):
HONEYBADGER_API_KEY=xxxxxxxx
HONEYBADGER_ENV=production
Bind the client once as a singleton so you get one instance per request:
// app/Providers/AppServiceProvider.php
use Honeybadger\Honeybadger;
public function register(): void
{
$this->app->singleton(Honeybadger::class, fn () => Honeybadger::new([
'api_key' => config('services.honeybadger.api_key'),
'environment' => app()->environment(),
'project_root' => base_path(),
'version' => config('app.version'),
'logger' => null, // see Gotchas: avoid log-loop
]));
}
Where to look first. The README's config table is the real API surface — the class itself is thin. Then read src/ in this order: the client entry point (Honeybadger::new() factory and notify()), the config/options object, the Handlers/ directory (error + exception handlers), and Filters/. The tests/ directory is usually the most complete usage documentation for this package, since the public docs are oriented toward the Rails gem and the Laravel wrapper.
First use case. Report unhandled exceptions with context. Hook into Laravel's exception handler rather than calling notify() from every controller:
// bootstrap/app.php (Laravel 11+) or app/Exceptions/Handler.php
public function register(): void
{
$this->reportable(function (Throwable $e) {
app(Honeybadger::class)->notify($e, [
'context' => ['user_id' => auth()->id()],
'tags' => [request()->route()?->getName()],
]);
});
}
Note: if you are on Laravel, check honeybadger-io/honeybadger-laravel first — it wraps this library and gives you a service provider, config file, and log channel out of the box. Use this core package directly when you need control over bootstrapping, are on a non-Laravel PHP app, or want a thinner dependency.
Wrap, don't scatter. Put a small ErrorReporter service in front of the client. It keeps notify() calls consistent and gives you one place to add tags, scrub data, and no-op in local/testing:
final class ErrorReporter
{
public function __construct(private Honeybadger $client) {}
public function report(Throwable $e, array $context = []): void
{
try {
$this->client->notify($e, [
'context' => $context + [
'user_id' => auth()->id(),
'request_id' => request()->header('X-Request-Id'),
'job' => $context['job'] ?? null,
],
]);
} catch (Throwable) {
// never let telemetry break the request
}
}
}
Middleware for request context. Set the user, route, and tenant once per request so every subsequent notify() carries it, rather than threading arrays through call sites.
Queue jobs. Exceptions in queued jobs never hit the HTTP exception handler. Use Queue::failing() or the JobExceptionOccurred event to report with job class, queue name, attempts, and payload ID (not the payload). This is the single highest-value integration after the exception handler — most production errors in a Laravel app live here.
Monolog channel. If the library ships a Monolog handler, registering it as a dedicated honeybadger log channel lets you report Log::channel('honeybadger')->error(...) alongside exception reporting. Keep it on its own channel, not stacked onto stack.
Filtering. Configure filters once at construction for passwords, tokens, Authorization headers, and card fields. Configure excluded exceptions for the noisy-but-expected classes: ValidationException, AuthenticationException, ModelNotFoundException, NotFoundHttpException. Without this you will burn quota and train your team to ignore the dashboard.
Custom (non-exception) notifications. For business-rule failures that aren't exceptions, either report a purpose-built exception subclass or use whatever custom-message API the current version exposes — check the README for the exact method, since this has changed between major versions. Tags plus a message string are what make these searchable later.
Environment gating. Report from staging and production; disable entirely in local and in test suites ('environment' => 'local' and a null reporter binding). Reporting from tests pollutes the project and slows the suite.
Testing. There is no Honeybadger::fake() here. Bind a test double in the container:
$this->swap(ErrorReporter::class, $spy = Mockery::spy(ErrorReporter::class));
// act...
$spy->shouldHaveReceived('report');
Assert on your own wrapper, not on the vendor client — that keeps tests stable across package upgrades.
'handler' => 'exception' configured, plus your own reportable() hook, every exception is sent two or three times. Pick exactly one reporting path.null or a null logger.JobProcessing, Octane's RequestReceived), or bind the client as scoped instead of singleton.project_root matters. Without it, stack traces show container paths and source snippets are omitted. Set it to base_path(); in Docker make sure the code is at the same path at report time as at deploy time.notify() call. Decide filtering at boot. Also, filtering is key-based and depth-limited — nested or serialized payloads can still leak. Don't put raw request bodies in context.excluded_exceptions is a literal class list. Subclass inheritance is not reliably followed; list the exact classes you want silenced, including framework subclasses.set_error_handler can conflict with Laravel's own handler and silently swallow warnings. Prefer exception reporting plus explicit notify() for the warnings you care about.composer.json and read the CHANGELOG before every bump; the README often documents only the latest API.endpoint and possibly disabling SSL verification. Verify connectivity with a single throwaway notify(new Exception('ping')) from staging before rolling out.random_int() sample for high-volume, low-severity paths.How can I help you explore Laravel packages today?