Canonical Laravel client library for reporting errors into kendo's error-tracking endpoint — scrubbing + auth + swallow-on-failure, installable via Composer across Script Development Laravel territories.
kendo ships the server endpoint, but allied projects must not POST raw HTTP: PII scrubbing has to happen source-side and consistently. This library is the gate — install it, call ErrorTracker::report($exception) from your exception handler, and inherit scrubbing, Bearer auth, path normalization, and swallow-on-failure for free. Without it, every consuming project reinvents the wheel and the scrubbing contract drifts.
composer require script-development/kendo-error-trackerThe ErrorTrackerServiceProvider is auto-discovered via Laravel package discovery — no manual registration. Publish the config if you want to tune it:
php artisan vendor:publish --tag=error-tracker-configSet the environment variables (the config reads ERROR_TRACKER_*). Only the first three are required — without them a report is silently dropped. Everything below them is optional and has a sane default.
| Env var | Config key | Required? | Description |
|---|---|---|---|
ERROR_TRACKER_KENDO_URL |
kendo_url |
Required | Base URL of your kendo tenant — always https://{tenant}.kendo.dev (e.g. https://script.kendo.dev). |
ERROR_TRACKER_PROJECT |
project |
Required | The kendo project id that owns the errors (the {project} route-key; kendo binds it by id). |
ERROR_TRACKER_TOKEN |
token |
Required | A kendo project token carrying the error-events:write ability (Bearer). |
ERROR_TRACKER_ENVIRONMENT |
environment |
Optional | Deploy environment label. May be omitted — falls back to APP_ENV, then production. Only set it to override that derived default. |
ERROR_TRACKER_RELEASE |
release |
Optional | Release identifier (git sha / version tag). May be omitted — when unset it is dropped from the payload entirely. |
ERROR_TRACKER_SYNC |
sync |
Optional | false (default) queues the report; true POSTs inline. |
ERROR_TRACKER_CONNECT_TIMEOUT |
connect_timeout |
Optional | Seconds to wait while connecting to the kendo host (default 2). |
ERROR_TRACKER_TIMEOUT |
timeout |
Optional | Total seconds to wait for the POST (default 5); bounds the call so a hung host never blocks the caller. |
Minimal working config — just the three required vars:
ERROR_TRACKER_KENDO_URL=https://script.kendo.dev
ERROR_TRACKER_PROJECT=7
ERROR_TRACKER_TOKEN=your-project-tokenThe optional knobs below are shown with their defaults; leave them commented out unless you need to override:
# ERROR_TRACKER_ENVIRONMENT= # defaults to APP_ENV, then "production"
# ERROR_TRACKER_RELEASE= # omitted from the payload when unset (e.g. v1.2.3)
# ERROR_TRACKER_SYNC=false # true POSTs inline instead of queueing
# ERROR_TRACKER_CONNECT_TIMEOUT=2 # seconds to wait while connecting
# ERROR_TRACKER_TIMEOUT=5 # total seconds to wait for the POSTThe token is a kendo project token carrying the error-events:write ability:
- Open the kendo project's API token settings.
- Create a token scoped to the project and grant it the
error-events:writeability. - Copy the token into
ERROR_TRACKER_TOKEN.
The token is bound to the project it was minted under. A token used against a different project's route is rejected by the server (422) — and like every failure, the client swallows it.
Report exceptions from your application's exception handler. In Laravel 11+ (bootstrap/app.php):
use ScriptDevelopment\KendoErrorTracker\ErrorTracker;
use Throwable;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->report(function (Throwable $e): void {
app(ErrorTracker::class)->report($e);
});
})Or from a classic App\Exceptions\Handler::report():
public function report(Throwable $e): void
{
app(\ScriptDevelopment\KendoErrorTracker\ErrorTracker::class)->report($e);
parent::report($e);
}That single call is the whole integration. report() is swallow-on-failure: it never throws and never blocks the request, so it is safe to call from inside your own exception handler.
report() builds and POSTs this body to {kendo_url}/api/projects/{project}/error-events:
{
"environment": "production",
"release": "v1.2.3",
"exception_class": "RuntimeException",
"message": "<scrubbed exception message>",
"stack_trace": "<scrubbed, path-normalized stack trace>"
}environment reflects the resolved value (your ERROR_TRACKER_ENVIRONMENT, else APP_ENV, else production); release is omitted from the body entirely when unset. No request, user, or context fields are sent — the server schema bans them.
Before send, the message and stack trace are scrubbed of the following patterns (each replaced with a [REDACTED:<kind>] marker):
| Pattern | Example |
|---|---|
| JWT | eyJhbGc... (three base64url segments) |
| Bearer token | Bearer <credential> |
| BSN (Dutch citizen service number) | a 9-digit run |
| Email address | [email protected] |
Each stack frame's absolute path has the app's own base_path() stripped (an exact prefix removal, mirroring laravel/nightwatch's Location::normalizeFile()). The same exception thrown from /var/www/html/app/Foo.php and /home/forge/app/Foo.php normalizes to the identical app/Foo.php, so kendo fingerprints it once regardless of deploy root.
- Async (default):
report()dispatchesReportErrorJobto the queue. The job has 0 retries — a failed POST logs to the local PHPerror_logand is never requeued, so error tracking never amplifies load during an outage. - Sync: set
error-tracker.sync(ERROR_TRACKER_SYNC=true) to POST inline.
Both modes swallow every failure.
A 202 response is success. Every failure — HTTP timeout, 401 (no/invalid token), 403 (token lacks error-events:write), 422 (token not linked to the project, or revoked), 5xx, or an unreachable host — is written to the local error_log and never thrown.
composer test # Pest
composer phpstan # PHPStan (level max, self-analysis)
composer format:check # Pint --test
composer format # Pint write