Skip to content

Error Tracking

kendo collects exceptions from your applications, fingerprints each one, and deduplicates identical errors into a single error group on your project — with an occurrence count and first/last-seen timestamps. This guide wires an application up to report into kendo end to end: mint a token, send errors, and triage them.

For Laravel applications, use the canonical client library, kendo-error-tracker. For any other language or framework, post raw HTTP to the Error Events API.

Requires the Error Tracking feature

Error ingestion is only available when the Error Tracking feature is enabled for your workspace. If it is disabled, requests return 403 with "feature_restricted": true.

Why a client library, not raw HTTP?

The package is more than convenience — it enforces a contract a hand-rolled POST can't:

  • Source-side PII scrubbing. Messages and stack traces are scrubbed before they leave your server. JWTs, Bearer tokens, BSNs (Dutch citizen service numbers), and email addresses are each replaced with a [REDACTED:<kind>] marker. Scrubbing has to happen at the source and consistently — a raw POST ships whatever the exception happened to carry.
  • Exact path normalization. Each stack frame's absolute path has your app's own base_path() stripped, so the same exception thrown from /var/www/html/app/Foo.php and /home/forge/app/Foo.php both arrive as app/Foo.php and fingerprint into one group.
  • Async, swallow-on-failure delivery. Reports dispatch to the queue with 0 retries; a failed send logs locally and is never requeued. report() never throws and never blocks the request that hit the error — error tracking can't become the thing that slows down or breaks your app.

This is why allied projects should not POST raw HTTP when a client exists: the scrubbing contract would drift, and PII could leak server-to-server. The raw endpoint remains available for languages the client doesn't cover — see Raw HTTP below.

1. Mint a project token

Error ingestion authenticates with a project token carrying the error-events:write ability:

  1. Open the project and go to Settings (/projects/{projectId}/settings).
  2. Scroll to the API Tokens section.
  3. Click Create Token, name it (e.g. Production error reporter), and set the Ability to Error tracking.
  4. Click Create and copy the token — it is shown only once.

The token is bound to that one project. Used against a different project's route, the server rejects it (422); the client swallows that like any other failure.

2. Install the client (Laravel)

bash
composer require script-development/kendo-error-tracker

The ErrorTrackerServiceProvider is auto-discovered through Laravel package discovery — no manual registration. Publish the config if you want to tune it:

bash
php artisan vendor:publish --tag=error-tracker-config

Configure

Set the environment variables (the config reads ERROR_TRACKER_*):

dotenv
ERROR_TRACKER_KENDO_URL=https://script.kendo.dev
ERROR_TRACKER_PROJECT=7
ERROR_TRACKER_TOKEN=your-project-token
ERROR_TRACKER_ENVIRONMENT=production
ERROR_TRACKER_RELEASE=v1.2.3
Env varConfig keyDescriptionDefault
ERROR_TRACKER_KENDO_URLkendo_urlBase URL of your kendo tenant — https://{tenant}.kendo.dev
ERROR_TRACKER_PROJECTprojectThe kendo project id that owns the errors (the {project} route key — kendo binds it by id)
ERROR_TRACKER_TOKENtokenA project token with the error-events:write ability (sent as a Bearer token)
ERROR_TRACKER_ENVIRONMENTenvironmentDeploy environment label sent with each eventAPP_ENV
ERROR_TRACKER_RELEASEreleaseOptional release identifier (git sha / version tag)null
ERROR_TRACKER_SYNCsyncfalse queues the report; true POSTs inlinefalse
ERROR_TRACKER_CONNECT_TIMEOUTconnect_timeoutSeconds to wait while connecting to the kendo host2
ERROR_TRACKER_TIMEOUTtimeoutTotal seconds to wait for the POST5

ERROR_TRACKER_PROJECT is the numeric project id, not its code — it is the {project} segment in {kendo_url}/api/projects/{project}/error-events.

Report exceptions

Report from your application's exception handler. In Laravel 11+ (bootstrap/app.php):

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():

php
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.

Dispatch modes

  • Async (default). report() dispatches a queued job (0 retries) — a failed POST logs to the local error_log and is never requeued, so error tracking never amplifies load during an outage.
  • Sync. Set ERROR_TRACKER_SYNC=true to POST inline in the request that reported. Both modes swallow every failure.

Raw HTTP (non-Laravel)

For systems the client doesn't cover, post directly to the Error Events API. You are then responsible for scrubbing PII and normalizing paths yourself — the server applies a best-effort path-prefix fallback, but it can't scrub message contents or know your deploy root the way the client does.

bash
curl -X POST https://{tenant}.kendo.dev/api/projects/1/error-events \
  -H "Authorization: Bearer your-project-token" \
  -H "Content-Type: application/json" \
  -d '{
    "environment": "production",
    "release": "v1.4.2",
    "exception_class": "RuntimeException",
    "message": "Undefined array key \"user_id\"",
    "stack_trace": "#0 /var/www/app/Http/Controllers/OrderController.php(42): handle()\n#1 {main}"
  }'

A successful ingest returns 202 Accepted with an empty body. See the Error Events API for the full field reference, validation limits, and error codes.

What gets sent

Both paths POST the same body to {kendo_url}/api/projects/{project}/error-events:

json
{
  "environment": "production",
  "release": "v1.2.3",
  "exception_class": "RuntimeException",
  "message": "<scrubbed exception message>",
  "stack_trace": "<scrubbed, path-normalized stack trace>"
}

release is omitted when unset. No request, user, or context fields are sent, and any extra fields in the body are ignored — kendo deliberately does not persist request bodies or arbitrary context with error groups.

Where errors land — and triage

Ingested events become error groups on the project. Identical errors (same exception class and normalized top stack frame) collapse into one group with an occurrence count, so a fault firing thousands of times shows up once. Each group tracks first_seen_at, last_seen_at, and the latest message and stack trace.

Error groups are operational data and are pruned automatically: a group is deleted once its last_seen_at is older than the retention window (default 90 days). Active errors that keep occurring are never pruned.

See Also