mindscape/raygun4php
Raygun4PHP is a PHP client for Raygun error tracking. It captures exceptions and errors, sends stack traces, tags, custom data, and user context, and works with Laravel or plain PHP apps to provide real-time crash reporting and diagnostics.
Install
composer require mindscape/raygun4php
Requirements are ext-curl, ext-json, and a reasonably modern PHP (7.4/8.x depending on version). There is no Laravel service provider, facade, config file, or publishable assets — you wire it yourself.
Where to look first
src/RaygunClient.php — the entire public API surface (SendException, SendError, SetUser, SetTags, SetVersion, SetCustomData, SetCustomDataProvider, SetFilterParams, SetAsyncSending/DisableAsyncSending, SetProxy, ignoreErrors).src/RaygunRequestMessage.php — this is the file that decides what gets scraped out of $_SERVER, $_POST, $_COOKIE, $_SESSION, and request headers. Read it before you ship to production; it is effectively your privacy/data-leak audit.src/RaygunMessage.php — the JSON envelope sent to https://api.raygun.io/entries.First use case: report unhandled exceptions
Bind a singleton so the client is configured once per process:
// app/Providers/AppServiceProvider.php
use Raygun4Php\RaygunClient;
$this->app->scoped(RaygunClient::class, function () {
$client = new RaygunClient(config('services.raygun.key'));
$client->SetVersion(config('app.version', 'dev'));
$client->SetTags([config('app.env'), gethostname()]);
$client->SetFilterParams(config('services.raygun.filter', []));
if (app()->runningInConsole() || app()->runningUnitTests()) {
$client->DisableAsyncSending();
}
return $client;
});
Laravel 11+ (bootstrap/app.php):
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (Throwable $e) {
try {
app(\Raygun4Php\RaygunClient::class)->SendException($e);
} catch (\Throwable $ignored) {
// never let the reporter break the request
}
});
})
Laravel 10 and below: call the same block from App\Exceptions\Handler::report() after parent::report($e).
That alone gets you stack traces, request data, and environment info in the Raygun dashboard within one deploy. Everything else is enrichment.
Attach the authenticated user in middleware, not the provider. The client is stateful and, under Octane, a container singleton outlives the request.
public function handle($request, Closure $next)
{
$client = app(RaygunClient::class);
if ($user = $request->user()) {
$client->SetUser([
'Identifier' => (string) $user->getKey(),
'IsAnonymous' => false,
'Email' => $user->email,
'FullName' => $user->name,
'FirstName' => Str::before($user->name, ' '),
]);
}
return $next($request);
}
Use scoped() bindings (or forgetInstance in a terminating middleware) so user/tags/custom data don't bleed between requests under Octane or in tests.
Lazy context via SetCustomDataProvider. Pass a closure; it is invoked at send time, so the client always reports the current state instead of whatever existed when it was bound:
$client->SetCustomDataProvider(function () {
return [
'tenant_id' => tenant()?->id,
'route' => request()->route()?->getName(),
'queue' => app()->runningInConsole() ? 'cli' : null,
];
});
Rich tags drive grouping. Tags are plain strings and are how you'll slice issues (e.g. ['api', 'billing', 'v2']). Set static tags once at bind time, then push per-feature tags right before a manual SendError.
Manual reporting for non-exception events. SendError($message, $tags, $customData) takes a string — use it for handled-but-serious conditions (payment gateway declines, webhook signature failures, third-party API non-200s) that you don't want to throw:
app(RaygunClient::class)->SendError(
'Stripe webhook signature mismatch',
['billing', 'webhook'],
['event_id' => $payload['id'] ?? null]
);
Version tracking. Feed SetVersion(config('app.version')) (or a git SHA injected at build). Raygun's "Deployment" view is only useful if this changes per release — it's the fastest way to correlate a spike to a deploy.
Console/queue jobs. Async curl handles won't reliably flush at the end of a long-lived worker. Disable async in console mode (as above) and add job context manually — SendException will otherwise capture CLI-shaped "request" data that tells you nothing:
$client->SetCustomData(['job' => $job::class, 'attempt' => $job->attempts(), 'queue' => $job->queue]);
Wrap it if you plan to test. The package has no HTTP abstraction, so you can't Http::fake() it. Put a small ErrorReporter interface over it and bind a no-op fake in testing, or subclass RaygunClient and override the protected Send() method. This is also your escape hatch if you later migrate to Sentry/Bugsnag.
Filtering. Build SetFilterParams([...]) from a config array that includes Laravel's usual suspects (password, password_confirmation, token, secret, api_key, authorization, card_number, cvv) plus your domain-specific keys. Do this in the same place you bind the client so it can't be forgotten.
__destruct. The client opens curl_multi handles and completes them on destruction. That's fine in PHP-FPM, fragile under exit()/dd(), and unreliable in queue workers and Octane. Call DisableAsyncSending() in CLI contexts, and know that fatal errors (E_ERROR, memory exhaustion) frequently never reach Raygun because the process dies before __destruct runs. If fatal-error coverage matters, add a register_shutdown_function that inspects error_get_last() and reports synchronously.RaygunRequestMessage walks $_SERVER, $_POST, $_COOKIE, $_SESSION, and headers. That includes Authorization: Bearer …, cookies, and any secrets Laravel exposes via env into $_SERVER. SetFilterParams() masks by key name recursively — it does not scrub free-text values, so a token inside a JSON string body sails straight through.SetUser keys are exact and case-sensitive: Identifier, IsAnonymous, Email, FullName, FirstName. identifier or user_id are silently dropped — a very common reason "user shows as anonymous."json_encode-able. Resources, closures, circular Eloquent relations, or Binary data will cause the encode to fail and the field to vanish without an error. Always reduce to scalars: $model->only(['id', 'status']).Handler::report() and register a Log listener or a middleware try/catch, you'll get duplicate issues and inflated counts. Pick one chokepoint. Also add $exceptions->dontReport([ValidationException::class, AuthenticationException::class, ModelNotFoundException::class]) — 404s and validation failures are noise and will burn your quota.ignoreErrors() is for PHP error codes only (E_NOTICE, E_DEPRECATED…), not exception classes. Exception filtering is your job.json_encode failure should never cascade. Wrap every send in try/catch (\Throwable).Raygun4Php\RaygunClient; most blog posts and Stack Overflow answers use the global RaygunClient. If you're following a tutorial and get "class not found," that's why.if (app()->environment('local', 'testing')) return; at the binding level, or set a short CURLOPT_TIMEOUT via a subclass. SetProxy() exists if you route egress through a gateway.OccurredOn is built from PHP's default timezone. If your server is UTC and your team reads in local time, timestamps look hours off — set date_default_timezone_set() explicitly or accept UTC everywhere.SendException in a simple per-request cap or a cache-based limiter.composer.json, read the changelog before bumping, and keep the client behind your own thin interface so swapping providers is an afternoon, not a refactor. Also confirm it stays compatible with your PHP version — this package does not track Laravel releases.debug/dry-run mode. To confirm wiring without polluting the dashboard, point the client at a scratch project key or temporarily var_dump the payload from a subclassed Send().How can I help you explore Laravel packages today?