Report your Laravel app's exceptions to Cooee, and answer its health checks.
gonbi-digital/laravel-cooee is a Laravel package for report your laravel app's exceptions to cooee, and answer its health checks..
It currently has 0 GitHub stars and 97 downloads on Packagist (latest version v0.3.0).
Install it with composer require gonbi-digital/laravel-cooee.
Discover more Laravel packages by gonbi-digital
or browse all Laravel packages to compare alternatives.
Last updated
Report your Laravel app's exceptions to Cooee.
Install it, set two values in .env, done: every exception Laravel would have logged is grouped
into an issue on the monitor's Errors tab, and the people on the monitor's alerts hear about a
new one, or a fixed one coming back.
Cooee checks a site from the outside. An exception inside the app is invisible to that: the page still answers 200 and a checkout that throws on every third order looks healthy from the street. This package is the app telling.
It also answers the other way: set a health token and the app serves a /health endpoint that
Cooee's monitor polls, checking the database, cache, storage and queue from the inside, and a
/health/ops page for the people fixing it. See Health checks.
PHP 8.2+, Laravel 12 or 13.
composer require gonbi-digital/laravel-cooee
In Cooee, edit the monitor, press Generate beside Error reporting token, copy it, save. Then
open the monitor's Errors tab and copy the ingest address. Put both in .env:
COOEE_ERRORS_URL=https://cooee.example/api/v1/ingest/monitors/12/errors
COOEE_ERRORS_TOKEN=the-token-you-generated
Check the wiring:
php artisan cooee:test
That sends one CooeeTestException, prints what Cooee answered, and opens an issue on the Errors
tab that you can mark fixed. Nothing has to be added to bootstrap/app.php: the package hooks
the exception handler itself.
Everything Laravel reports. That is the framework's own rule, so validation failures, 404s and
authentication and authorisation exceptions never reach Cooee, and anything you add to
$exceptions->dontReport([...]) is left out too. report($e) reports as well.
Each report carries the exception class, message, file and line (relative to the app root), the
first thirty stack frames, the request URL and method when there was one, the environment, the
release, and a small context: the signed-in user's id (only the id, and only if
include_user stays on), the route name, the queued job class or the artisan command name, and
the previous exception if there was one.
Reports leave after the response has been sent, so a slow or absent Cooee costs the visitor nothing. In a job or a command they go inline and take at most the timeout, two seconds, and only when something has already gone wrong. The package never throws.
Publish the config if you want to change anything beyond the two env values:
php artisan vendor:publish --tag=cooee-config
| Key | Env | Default | What it does |
| ------------------- | ------------------- | ----------------------------------- | --------------------------------------------------------------- |
| url, token | COOEE_ERRORS_* | none | Where to report and with what. Missing either, nothing is sent |
| enabled | COOEE_ENABLED | off in local and testing | The whole thing on or off |
| environment | COOEE_ENVIRONMENT | app()->environment() | Shown on each occurrence |
| release | COOEE_RELEASE | config('app.version') | Shown on each occurrence |
| ignore_exceptions | | [] | Classes never sent, subclasses included, on top of dontReport |
| include_user | | true | Attach the signed-in user's id |
| defer | | true | Send after the response rather than during it |
| timeout | | 2 | Seconds the HTTP call may take |
| trace_frames | | 30 | Frames to send |
Report something you caught yourself, with context of your own:
use GonbiDigital\Cooee\Facades\Cooee;
try {
$gateway->charge($order);
} catch (GatewayException $e) {
Cooee::report($e, ['order_id' => $order->id, 'gateway' => 'stripe']);
}
Attach context to every report from a service provider or middleware:
Cooee::context(['tenant' => $tenant->slug]);
Change or drop a report on its way out. Return the payload to send it, or null to drop it:
Cooee::beforeSend(function (array $payload, Throwable $e): ?array {
if ($e instanceof TooNoisyException) {
return null;
}
unset($payload['context']['user_id']);
return $payload;
});
Cooee::fake() keeps reports in memory and switches reporting on, so a test can say that a
failure is reported without any env and without a network:
use GonbiDigital\Cooee\Facades\Cooee;
it('reports a failed charge', function () {
$cooee = Cooee::fake();
$this->post('/checkout', [...])->assertStatus(500);
$cooee->assertReported(GatewayException::class, fn (array $payload) => $payload['context']['gateway'] === 'stripe');
});
Also assertNotReported($class), assertNothingReported() and assertReportedCount($n).
The other direction: Cooee asking the app how it is. Pick a long random string, put it in .env,
and paste the same value into the monitor's Token field in Cooee:
COOEE_HEALTH_TOKEN=some-long-random-string
With a token set, the package registers two routes. Without one it registers nothing, so an app that only wants error reporting gains no routes.
| Route | For | Shape | Gate | Middleware |
| ------------- | --------- | ---------------------- | -------------------------- | ---------------- |
| /health | a monitor | the JSON contract | bearer token, else 404 | none |
| /health/ops | a person | a standalone HTML page | signed in, or the token | web + own gate |
/health carries no middleware at all on purpose. With SESSION_DRIVER=database, a route inside
the web group boots a session out of the database it is being asked about, and the moment that
database goes away the monitor gets a 500 with no explanation instead of database: SQLSTATE[HY000] [2002] Connection refused. The token is the credential, sent as Authorization: Bearer ... or,
from a browser, as ?token=....
{
"status": "ok",
"checks": {
"database": { "status": "ok", "message": "mysql · app_production", "ms": 3 },
"cache": { "status": "ok", "message": "read back what was written to redis", "ms": 1 },
"storage": { "status": "ok", "message": "read back what was written to the s3 disk", "ms": 41 },
"queue": { "status": "degraded", "message": "1204 jobs pending, above the 1000 threshold", "ms": 2 }
}
}
status is ok, degraded or down, on the report and on each check, and the overall status is
the worst component. ok and degraded answer 200, down answers 503: a queue backlog is
worth knowing about, not worth declaring an outage over.
| Check | What it actually does |
| ---------- | -------------------------------------------------------------------------------------------- |
| database | select 1. Not getPdo(), which hands back a connection opened before the server left. |
| cache | Writes a token, reads it back, compares. A failed-over Redis accepts writes and returns null. |
| storage | Writes a file, reads it back, deletes it in a finally, so a probe cannot litter a bucket. |
| queue | Pending depth and the age of the oldest waiting job, on the database driver only. |
Either queue threshold crossing is degraded, never down. Switch any check off under
health.checks in config/cooee.php where it cannot mean anything.
/health/ops shows the checks, the drivers the app runs on, whether optimize ran, and the
release, commit, branch and deploy time when there are any (COOEE_RELEASE or app.version, a
RELEASE file, and GIT_SHA / GIT_BRANCH / DEPLOYED_AT). It is standalone Blade with inline
CSS, so it renders on the deploy that broke the frontend build. Publish it with
--tag=cooee-views to change it.
It does not use Laravel's auth middleware, which redirects to a route named login that a
Filament app does not have. A stranger gets a 404. Any signed-in user (on any guard, narrow it
with health.page.guards) or the health token gets in, the token being the only way in when the
login system is itself what broke. Set health.page.allow_authenticated to false for token only.
| Key | Env | Default | What it does |
| --------------------- | ------------------------ | ---------------- | ----------------------------------------------------- |
| health.token | COOEE_HEALTH_TOKEN | none | The shared secret. HEARTBEAT_TOKEN is read too |
| health.enabled | COOEE_HEALTH | when a token is set | Register the routes. true without a token leaves /health open: local only |
| health.path | COOEE_HEALTH_PATH | health | Where the JSON endpoint lives |
| health.page.enabled | COOEE_HEALTH_PAGE | true | Set false to register only the endpoint |
| health.page.path | COOEE_HEALTH_PAGE_PATH | health/ops | Where the ops page lives |
| health.queue.* | | 1000, 900 | Pending jobs and seconds waited before degraded |
| health.storage.disk | | the default disk | The disk the storage check writes to |
This replaces gonbi-digital/laravel-heartbeat. Remove that package and require this one. The
routes stay at /health and /health/ops, and HEARTBEAT_TOKEN is still read, so the monitor
keeps working without a change. Then:
HEARTBEAT_TOKEN to COOEE_HEALTH_TOKEN when convenient.HEARTBEAT_API_PATH, HEARTBEAT_PAGE and HEARTBEAT_PAGE_PATH to COOEE_HEALTH_PATH,
COOEE_HEALTH_PAGE and COOEE_HEALTH_PAGE_PATH if you set them. HEARTBEAT_API=false becomes
COOEE_HEALTH=false.config/heartbeat.php into the health key of config/cooee.php, with api
flattened into health and web renamed to page.cooee.health and cooee.health.page, the view is cooee::health.composer install
composer test # pint --test, phpstan, pest
MIT.