alex-kassel/laravel-actionable-diagnostics
Laravel Actionable Diagnostics brings structured health checks to your app, surfacing issues in configuration, environment, and services with clear, actionable fixes. Catch problems early and resolve them faster with diagnostics that tell you exactly what to do next.
This package gives you two things you wire up once and then lean on constantly: a diagnostic event engine that fans a single event out to multiple channels (webhooks among them), and an actionable exception framework so thrown exceptions carry enough context to be acted on rather than just logged.
Minimal path:
composer require the package, then publish its config:
php artisan vendor:publish --provider="...\ActionableDiagnosticsServiceProvider"
(Check vendor/.../config/actionable-diagnostics.php for the exact publish tag.)config/actionable-diagnostics.php first — this is the package's whole contract. v1.0.2 standardized it at the top level, so read the channel list, webhook settings, and the routes block before writing any code.actionable-diagnostics.routes.enabled off initially. You do not need the HTTP ingestion endpoint to get value; the outbound/webhook path and the exception framework work without it.Where to look first, in order: config/actionable-diagnostics.php → the package README Quick Links (added in v1.0.3, points at the exception contract and channel docs) → WebhookDispatcher source, since that's where the actual HTTP behavior lives.
Wrap, don't replace, your exception handler. The actionable exception framework is designed to sit inside Laravel's existing handler, not to be a second one. In bootstrap/app.php:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->reportable(function (ActionableDiagnostic $e) {
// context/actions travel with the exception itself
});
})
Keep your normal report()/render() behavior; let actionable exceptions add structure on top.
Throw domain exceptions that carry their own remediation. The framework's point is that a PaymentDeclinedException knows what the operator should do. Encode that at the throw site (context array, suggested action, severity) rather than reconstructing it later in a log parser. This is the single highest-leverage habit with this package: the throw site is where diagnostics are cheap.
Treat the event engine as a fan-out, not a loop. Dispatch one diagnostic event; let the configured channels decide delivery. Resist the temptation to call the webhook dispatcher directly from business logic — that couples you to one channel and bypasses the engine's routing/severity rules. If you need a new destination, add it as a channel in config rather than sprinkling dispatch calls.
Webhooks in tests are now trivially fakeable. v1.0.2 refactored WebhookDispatcher onto Illuminate\Support\Facades\Http, which means:
Http::fake(['*' => Http::response([], 200)]);
// ... trigger the failure ...
Http::assertSent(fn ($request) => $request['severity'] === 'critical');
This is the biggest day-to-day quality-of-life change in this release. Assert on the payload content of your diagnostics, not just that an exception was reported — that's where regressions hide.
Gate the ingestion routes like an admin endpoint. When you do enable actionable-diagnostics.routes.enabled, set routes.prefix to something non-obvious and put auth middleware in front of it. These routes accept diagnostic inbound traffic; on a public host they are an abuse vector. Pattern: enable only on staging/worker hosts, or behind auth:sanctum + IP allowlist.
Queue the outbound side for anything user-facing. A synchronous webhook in a request cycle adds another service's latency to your own. Dispatch diagnostics from queued jobs/after-response where possible.
routes.enabled or prefix, run php artisan config:clear (or config:cache in a deploy step). A stale cached config is the most likely reason "the routes don't exist."Http::preventStrayRequests() breaks the dispatcher in test suites. If your base TestCase blocks stray requests, the webhook channel will throw instead of silently no-op'ing. Either Http::fake() the diagnostic host in setUp(), or explicitly allow it.Http facade refactor means real timeouts and retries are now yours. The dispatcher no longer hides its transport. Set explicit connect/read timeouts in config; a hung diagnostic endpoint should never hold a worker. Consider ->retry() semantics carefully — a retried diagnostic can duplicate alerts on the receiving side.array<int, Channel>, iterable<string, mixed>) to be enforced on your side too. Follow the package's array-shape conventions or you'll be fighting @phpstan-ignore lines..gitattributes export-ignore means the installed package has no tests. You can't read the package's own test suite from vendor/ to learn intended usage — read it on GitHub instead. This is a docs-navigation gotcha, not a bug.Http::fake() + Http::assertSent in a tinker/test, then Http::fake(fn () => Http::response()) to inspect the serialized payload. If a webhook "isn't arriving," verify the event was dispatched before suspecting the transport — channel misconfiguration in actionable-diagnostics.php silently drops events more often than HTTP fails.WebhookDispatcher; the transport was just standardized, and forks of it will drift.^1.0. Note v1.0.3 is docs/packaging-only (README header, badges, release-gate file) and changes nothing at runtime.How can I help you explore Laravel packages today?