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 Read & write project token:

  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 Access to Read & write.
  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 (403); 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 Read & write project token (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.

Multi-tenant apps ​

In an app with one database per tenant, user ids restart in each tenant, so user 42 of two tenants would send one user hash and count as one user. From kendo-error-tracker v0.2.1, register a resolver that returns the current tenant key, once, in a service provider's boot():

php
app(ErrorTracker::class)->scopeUserHashUsing(fn (): int|string|null => tenant()?->getTenantKey());
  • The tenant key goes into the hash's key and is never sent, so the same user id in two tenants gives two hashes.
  • The resolver runs when a report is made. Return the tenant your app already resolved for the request; do not look it up in a database or a cache, because that lookup can be what failed.
  • When it returns null (no tenant), the hash is the same as without a resolver. When it throws, or returns anything but an integer or a non-empty string, no hash is sent; the report still is.

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. kendo-error-tracker v0.2 also sends each optional field it can read: the cause chain, the runtime, the framework, memory use, the context, the host and, for a request with a signed-in user, a keyed user hash (see Request Fields). Any other 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.

An event may also name the host that ran it and carry a keyed user hash: the HMAC-SHA256 of the signed-in user's id, keyed with a secret only your app holds. A group then counts its events per host and how many distinct users it hit. The hash takes the place of the user id: kendo cannot turn it back into one, and its API returns only the number of distinct hashes, never a hash. A value that is not a well-formed hash (a raw id, an email address) is dropped, and the event is still stored. The hash covers that one field: the message and the stack trace are stored as sent, which is why they are scrubbed before they leave your server (see Raw HTTP for a raw POST). See Affected users.

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. A group's user hashes and hosts each expire once they have not been seen for the length of that window, even while the group keeps firing, so a user who has not hit the error within the window no longer counts.

Create an issue from an error ​

Once you can see error groups in kendo, you can turn any of them into a tracked Bug issue in one click — no copy-pasting needed.

On the error group detail page, click Create issue next to the "Stack trace" heading. kendo creates a Bug issue pre-filled with:

  • Title: {ExceptionClass}: {latest message} (truncated to 255 characters)
  • Description: stack trace, occurrence count, environment, first/last seen timestamps, and a back-link to the error group

After the issue is created, the button is replaced by a "Tracked in KD-XXXX →" link that navigates directly to the issue. The issue page shows a corresponding "Originated from error" section linking back to the group.

Status and tracking are independent. Creating a Bug issue does not change the error group's status — an Open error stays Open until you explicitly resolve it. You can resolve, ignore, or reopen an error group regardless of whether a linked issue exists.

Already linked? If an error group already has a linked issue, the "Create issue" button is replaced by the tracked-issue link — you cannot create a second issue from the same group.

The underlying API endpoint is POST /api/projects/{projectId}/error-groups/{errorGroupId}/promote — see the Error Groups API for the full reference.

See Also ​