nikolaynesov/laravel-serene

Graceful, noise-free, and rate-limited exception reporting for Laravel

Installs: 58

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/nikolaynesov/laravel-serene

0.0.5 2025-12-07 11:57 UTC

This package is auto-updated.

Last update: 2025-12-07 11:58:02 UTC


README

Latest Version on Packagist Total Downloads

Graceful, noise-free, and rate-limited exception reporting for Laravel. Stop spamming your error tracking service with duplicate errors and get meaningful insights into how many times errors occurred and how many users were affected.

Features

  • Rate Limiting: Automatically throttles duplicate errors to prevent spam
  • Affected User Tracking: Tracks all users affected by an error during the cooldown period
  • Intelligent Error Grouping: Interface-based grouping for exceptions with dynamic messages
  • Metrics & Stats: Provides occurrence and throttle statistics for each error
  • Memory Protection: Configurable caps prevent cache overflow in catastrophic scenarios
  • Provider Agnostic: Works with Bugsnag, Sentry, or any custom error reporter
  • Cache Driver Compatible: Works with any Laravel cache driver (Redis, Memcached, File, etc.)
  • Easy Integration: Simple facade interface and automatic Laravel discovery

Installation

Install the package via Composer:

composer require nikolaynesov/laravel-serene

The package will automatically register itself via Laravel's package discovery.

Publish Configuration

Publish the configuration file:

php artisan vendor:publish --tag=serene-config

This will create a config/serene.php file.

Environment Variables

Add these to your .env file to configure Serene:

# Error reporting cooldown period in minutes (default: 30)
SERENE_REPORTER_COOLDOWN=30

# Enable debug logging (default: false)
SERENE_REPORTER_DEBUG=false

# Maximum users to track per error (default: 1000)
SERENE_REPORTER_MAX_TRACKED_USERS=1000

# Maximum unique errors to track simultaneously (default: 1000)
SERENE_REPORTER_MAX_TRACKED_ERRORS=1000

All environment variables are optional. If not set, the defaults shown above will be used.

Using with Bugsnag

If you're using Bugsnag, install the Bugsnag Laravel package:

composer require bugsnag/bugsnag-laravel

Then configure Bugsnag according to their documentation.

Set the provider in config/serene.php:

'provider' => \Nikolaynesov\LaravelSerene\Providers\BugsnagReporter::class,

Using with Laravel Logs

To simply log errors to your Laravel logs:

'provider' => \Nikolaynesov\LaravelSerene\Providers\LogReporter::class,

Usage

Integration with Laravel Exception Handler

The recommended way to use Serene is to integrate it with Laravel's exception handler. This automatically reports all exceptions with user context:

<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Nikolaynesov\LaravelSerene\Facades\Serene;
use Throwable;

class Handler extends ExceptionHandler
{
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            Serene::report($e, [
                'user_id' => auth()->id(),
            ]);
        });
    }
}

This will automatically report all exceptions with the current user's ID, enabling proper user tracking and Bugsnag identification.

Basic Usage

You can also use the Serene facade directly in your code:

use Nikolaynesov\LaravelSerene\Facades\Serene;

try {
    // Your code that might throw an exception
    $user = User::findOrFail($id);
} catch (\Exception $e) {
    Serene::report($e);

    // Handle the error gracefully
    return response()->json(['error' => 'User not found'], 404);
}

Tracking Affected Users

Track which users are affected by an error:

use Nikolaynesov\LaravelSerene\Facades\Serene;

try {
    // Your code
    $this->processPayment($user, $amount);
} catch (\Exception $e) {
    Serene::report($e, [
        'user_id' => $user->id,
    ]);

    // Handle error
}

When the error is reported, the context will include:

  • affected_users: Array of user IDs affected during the cooldown period (up to max_tracked_users)
  • affected_user_count: Total number of affected users tracked
  • user_tracking_capped: Boolean indicating if the user tracking limit was reached

Important: If using Bugsnag, the first affected user will be set as the primary user for the error report, enabling proper user filtering and search in Bugsnag's Users tab. All affected users remain visible in the metadata.

Enhanced Bugsnag Integration

When using BugsnagReporter, you can provide additional context fields that will enrich the Bugsnag error report:

Serene::report($exception, [
    'user_id' => $user->id,
    'user_email' => $user->email,      // Sets Bugsnag user email
    'user_name' => $user->name,        // Sets Bugsnag user name
    'severity' => 'warning',            // Sets Bugsnag severity level
    'app_version' => config('app.version'), // Adds to app metadata tab
]);

Recognized fields for Bugsnag:

  • user_email - Sets email on Bugsnag user (visible in Users tab)
  • user_name - Sets name on Bugsnag user
  • severity - Sets error severity (error, warning, info)
  • app_version - Adds application version to metadata

All fields remain in the error_throttler metadata tab for reference.

Adding Custom Context

Add any additional context to help with debugging:

Serene::report($exception, [
    'user_id' => $user->id,
    'order_id' => $order->id,
    'payment_method' => 'stripe',
    'amount' => $amount,
]);

Error Grouping

Serene provides three ways to control how errors are grouped for throttling, from most flexible to automatic:

1. Groupable Exceptions (Recommended)

Implement the GroupableException interface on your custom exceptions to define automatic error grouping:

use Nikolaynesov\LaravelSerene\Contracts\GroupableException;

class PaymentException extends \Exception implements GroupableException
{
    public function getErrorGroup(): string
    {
        return 'payment-processing-error';
    }
}

// All PaymentExceptions are automatically grouped together
throw new PaymentException("Payment failed for user {$userId}");
throw new PaymentException("Payment declined for order {$orderId}");
// Both grouped as 'payment-processing-error' regardless of message

Benefits:

  • Solves the dynamic message problem automatically
  • No need to specify keys at call sites
  • Self-documenting exceptions
  • Works across your entire codebase

Example with dynamic grouping:

class ApiException extends \Exception implements GroupableException
{
    public function __construct(
        string $message,
        private string $endpoint
    ) {
        parent::__construct($message);
    }

    public function getErrorGroup(): string
    {
        return "api:{$this->endpoint}";
    }
}

// Automatically groups by endpoint
throw new ApiException('Timeout', 'stripe/charges');
throw new ApiException('Invalid request', 'stripe/charges');
// Both grouped as 'api:stripe/charges'

2. Explicit Custom Keys

Override grouping with an explicit key parameter for one-off cases:

// Group by API endpoint
Serene::report($exception, ['user_id' => $user->id], 'api:payment:stripe');

// Group by specific operation
Serene::report($exception, ['order_id' => $order->id], 'order-processing');

When to use: Third-party exceptions you can't modify, or temporary overrides.

3. Auto-Generated Keys

If no interface or explicit key is provided, errors are grouped by exception class + message hash:

// Auto-grouped by class and message
throw new RuntimeException('Database connection failed');
// Key: auto:runtimeexception:abc123

Hierarchy: Explicit key > GroupableException > Auto-generated

How It Works

Rate Limiting Logic

  1. First Occurrence: When an error occurs for the first time, it's immediately reported to your error tracking service
  2. Cooldown Period: A cooldown timer starts (default: 30 minutes)
  3. Subsequent Occurrences: If the same error occurs again during the cooldown:
    • It's not reported (preventing spam)
    • User IDs are collected if provided (up to max_tracked_users limit)
    • Occurrence and throttle counters are incremented
  4. After Cooldown: When the cooldown expires, the next occurrence will be reported with full statistics

Reported Context

When an error is reported, the following context is automatically added:

[
    'affected_users' => [1, 5, 12, 43],  // Array of user IDs (max 1000)
    'affected_user_count' => 4,           // Total affected users tracked
    'user_tracking_capped' => false,      // True if hit max_tracked_users limit
    'occurrences' => 15,                   // Total times error occurred
    'throttled' => 14,                     // Times error was throttled
    'reported_at' => '2025-12-05 18:30:00', // When reported
    'key' => 'auto:runtimeexception:abc123', // Error key
    // ... your custom context
]

Debug Logging

Enable debug logging to monitor throttling behavior:

# .env
SERENE_REPORTER_DEBUG=true

Or in config:

// config/serene.php
'debug' => true,

When enabled, Serene logs all activity:

// When an error is reported (info level)
[Serene] auto:runtimeexception:abc123 reported
{
    "affected_users": [1, 5, 12],
    "count": 3,
    "occurrences": 15,
    "throttled": 14
}

// When an error is throttled (debug level)
[Serene] auto:runtimeexception:abc123 throttled
{
    "occurrences": 16,
    "throttled": 15
}

Note: Keep debug logging disabled in production to avoid log clutter. Only enable when investigating throttling issues.

Creating Custom Providers

Simple Custom Provider

You can create your own error reporter by implementing the ErrorReporter interface:

<?php

namespace App\ErrorReporters;

use Nikolaynesov\LaravelSerene\Contracts\ErrorReporter;
use Throwable;

class SentryReporter implements ErrorReporter
{
    public function report(Throwable $exception, array $context = []): void
    {
        \Sentry\captureException($exception, [
            'extra' => $context,
        ]);
    }
}

Then configure it in config/serene.php:

'provider' => \App\ErrorReporters\SentryReporter::class,

Advanced Bugsnag Customization

For advanced Bugsnag integration beyond the built-in fields, extend the BugsnagReporter. Configure your custom reporter:

// config/serene.php
'provider' => \App\ErrorReporters\EnhancedBugsnagReporter::class,

This approach gives you complete control over the Bugsnag integration while maintaining Serene's rate limiting and user tracking features.

Advanced Configuration

Adjusting Cooldown Period

Change the cooldown period based on your needs:

# .env

# 5 minutes for high-frequency errors
SERENE_REPORTER_COOLDOWN=5

# 24 hours for low-frequency errors
SERENE_REPORTER_COOLDOWN=1440

# Default: 30 minutes
SERENE_REPORTER_COOLDOWN=30

Per-Environment Configuration

Configure different settings per environment using .env files:

# .env.local (development)
SERENE_REPORTER_COOLDOWN=5
SERENE_REPORTER_DEBUG=true
SERENE_REPORTER_MAX_TRACKED_USERS=100

# .env.production (production)
SERENE_REPORTER_COOLDOWN=30
SERENE_REPORTER_DEBUG=false
SERENE_REPORTER_MAX_TRACKED_USERS=1000

Or dynamically in config config/serene.php

Cache Memory Management

Serene provides two-level protection against cache memory issues:

1. Per-Error User Tracking Cap (max_tracked_users)

Limits the number of users tracked per individual error (default: 1000).

Memory calculation per error:

  • 1000 users × 8 bytes = ~8KB per error

When user_tracking_capped is true in the error context, it indicates a widespread issue affecting many users.

Adjust for high-traffic applications:

# .env
SERENE_REPORTER_MAX_TRACKED_USERS=100

2. Global Error Tracking Cap (max_tracked_errors)

Limits the total number of unique errors tracked simultaneously (default: 1000).

Total memory calculation:

  • 1000 unique errors × 8KB = ~8MB total (worst case with max users per error)
  • Safe for any Redis instance with 512MB+ RAM

How it works:

  • Tracks up to 1000 unique errors with full throttling enabled
  • When limit is reached, new errors are reported immediately without throttling (fail-open)
  • Expired errors are automatically cleaned up as their cooldown periods end
  • This prevents catastrophic memory usage if millions of unique errors occur

For catastrophic scenario protection:

# .env
# Guaranteed max memory: 1000 errors × 8KB = 8MB
SERENE_REPORTER_MAX_TRACKED_ERRORS=1000

# More aggressive cap for memory-constrained environments
SERENE_REPORTER_MAX_TRACKED_ERRORS=100  # Max 800KB

When tracking limit is reached:

  • Errors include tracking_limit_reached: true in context
  • Errors are still reported to Bugsnag (no silent failures)
  • Throttling is bypassed to prevent cache overflow
  • Once old errors expire, throttling resumes automatically

Testing

Run the test suite:

composer test

Run static analysis:

composer phpstan

Requirements

  • PHP 8.1 or higher
  • Laravel 10.x or 11.x

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Credits

License

The MIT License (MIT). Please see License File for more information.