philiprehberger / php-exception-reporter
Lightweight exception reporting to log channels and webhooks
Package info
github.com/philiprehberger/php-exception-reporter
pkg:composer/philiprehberger/php-exception-reporter
Fund package maintenance!
Requires
- php: ^8.2
Requires (Dev)
- laravel/pint: ^1.0
- phpstan/phpstan: ^1.12|^2.0
- phpunit/phpunit: ^11.0
Suggests
- psr/log: For PSR-3 log channel integration (^3.0)
README
Lightweight exception reporting to log channels and webhooks.
Requirements
- PHP 8.2+
Installation
composer require philiprehberger/php-exception-reporter
Usage
Basic reporting with a callback
use PhilipRehberger\ExceptionReporter\ExceptionReporter; use PhilipRehberger\ExceptionReporter\Channels\CallbackChannel; $reporter = new ExceptionReporter(); $reporter->addChannel(new CallbackChannel(function ($report) { error_log("[{$report->class}] {$report->message} in {$report->file}:{$report->line}"); })); try { riskyOperation(); } catch (\Throwable $e) { $reporter->capture($e); }
File channel
use PhilipRehberger\ExceptionReporter\Channels\FileChannel; $reporter->addChannel(new FileChannel('/var/log/app-exceptions.log')); // Each report is written as a JSON line $reporter->capture(new \RuntimeException('Something failed'));
Multiple channels
$reporter ->addChannel(new CallbackChannel(function ($report) { // Send to your monitoring service })) ->addChannel(new FileChannel('/var/log/exceptions.log'));
Deduplication
Prevent the same exception (same class, file, and line) from being reported more than once:
$reporter->enableDeduplication(); $exception = new \RuntimeException('flaky'); $reporter->capture($exception); // Reported $reporter->capture($exception); // Skipped (duplicate) $reporter->resetFingerprints(); // Clear dedup state $reporter->capture($exception); // Reported again
Adding context
$reporter->capture($exception, [ 'user_id' => 42, 'request_url' => '/checkout', ]);
Persistent context
Attach context fields that are included in every subsequent report. withContext() returns a new immutable instance:
$reporter = $reporter->withContext([ 'request_id' => 'abc-123', 'user_id' => 42, ]); // Both reports will include request_id and user_id $reporter->capture(new \RuntimeException('first')); $reporter->capture(new \LogicException('second')); // Per-call context is merged with persistent context $reporter->capture($exception, ['action' => 'checkout']);
Filtering exceptions
Skip certain exceptions from being reported using a filter callable:
$reporter->setFilter(function (\Throwable $e): bool { // Return false to skip reporting return !$e instanceof \DeprecationException; }); $reporter->capture(new \DeprecationException('old API')); // Skipped $reporter->capture(new \RuntimeException('real error')); // Reported
Webhook channel
Send exception reports to an HTTP endpoint:
use PhilipRehberger\ExceptionReporter\Channels\WebhookChannel; $reporter->addChannel(new WebhookChannel('https://example.com/webhook')); // With custom headers $reporter->addChannel(new WebhookChannel( 'https://example.com/webhook', ['Authorization' => 'Bearer secret'], )); // With a custom payload transformer $reporter->addChannel(new WebhookChannel( 'https://example.com/webhook', [], fn ($report) => ['text' => "[{$report->class}] {$report->message}"], ));
Rate limiting
Prevent flooding during cascading failures by limiting how many reports are dispatched:
$reporter->rateLimit(maxReports: 10, windowSeconds: 60); // Only the first 10 reports per fingerprint (and 10 globally) within 60 seconds are sent for ($i = 0; $i < 100; $i++) { $reporter->capture(new \RuntimeException('storm')); }
Summary reports
Access an aggregated summary of captured exceptions:
$reporter->capture(new \RuntimeException('db timeout')); $reporter->capture(new \RuntimeException('db timeout')); $reporter->capture(new \LogicException('bad state')); $summary = $reporter->summary(); $summary->totalCount(); // 3 $summary->uniqueCount(); // 2 $summary->since(); // DateTimeImmutable of earliest report $summary->until(); // DateTimeImmutable of latest report $summary->topExceptions(); // [{message, count, lastSeen}] sorted by frequency $reporter->clearHistory(); // Reset stored reports
Tracking report count
$reporter->capture(new \RuntimeException('one')); $reporter->capture(new \LogicException('two')); echo $reporter->count(); // 2
Custom channels
Implement the ReportChannel interface to build your own channel:
use PhilipRehberger\ExceptionReporter\Contracts\ReportChannel; use PhilipRehberger\ExceptionReporter\ExceptionReport; class SlackChannel implements ReportChannel { public function report(ExceptionReport $report): void { // POST to Slack webhook with $report->toArray() } }
API
ExceptionReporter
| Method | Description |
|---|---|
addChannel(ReportChannel $channel): self |
Register a reporting channel |
enableDeduplication(): self |
Enable fingerprint-based deduplication |
capture(Throwable $e, array $context = []): ExceptionReport |
Capture and report an exception |
resetFingerprints(): void |
Clear deduplication state |
withContext(array $context): self |
Return a new instance with persistent context fields |
setFilter(callable $filter): self |
Set a filter; return false to skip reporting |
count(): int |
Number of exceptions reported by this instance |
rateLimit(int $maxReports, int $windowSeconds = 60): self |
Enable rate limiting to cap reports per window |
summary(): ReportSummary |
Return an aggregated summary of stored reports |
clearHistory(): void |
Clear the stored report history |
ExceptionReport
| Property / Method | Description |
|---|---|
string $class |
Exception class name |
string $message |
Exception message |
string $file |
File where the exception was thrown |
int $line |
Line number |
string $trace |
Stack trace as string |
DateTimeImmutable $timestamp |
When the exception was captured |
array $context |
Additional context data |
?string $previousClass |
Previous exception class, if any |
?string $previousMessage |
Previous exception message, if any |
fingerprint(): string |
MD5 hash of class + file + line |
toArray(): array |
Serialize to array |
fromThrowable(Throwable, array): self |
Create from a throwable |
Channels
| Channel | Description |
|---|---|
CallbackChannel |
Invokes a user-provided callable |
FileChannel |
Appends JSON-encoded reports to a file |
WebhookChannel |
Sends JSON-encoded reports to an HTTP endpoint via POST |
RateLimiter
| Method | Description |
|---|---|
__construct(int $maxReports = 100, int $windowSeconds = 60) |
Create a rate limiter with per-fingerprint and global limits |
shouldReport(string $fingerprint): bool |
Check if a report should be allowed within the current window |
ReportSummary
| Method | Description |
|---|---|
totalCount(): int |
Total number of stored reports |
uniqueCount(): int |
Count of unique exception fingerprints |
topExceptions(int $limit = 5): array |
Top exceptions sorted by frequency with message, count, and lastSeen |
since(): ?DateTimeImmutable |
Earliest report timestamp |
until(): ?DateTimeImmutable |
Latest report timestamp |
Development
composer install vendor/bin/phpunit vendor/bin/pint --test vendor/bin/phpstan analyse
Support
If you find this project useful: