
# PHP error tracking

Error tracking is **on by default** in the PHP SDK (`errors.enabled`, environment variable `RANETRACE_ERRORS_ENABLED`), but it captures nothing until you install the handlers or report an exception yourself.

Using Laravel? See [Error Tracking](/docs/error-tracking) instead.

## Automatic capture

```php
$ranetrace->registerErrorHandlers();
```

That one call installs two handlers, because uncaught exceptions and fatal errors are two different failure modes:

- **An exception handler**, for exceptions nothing caught. Whatever exception handler your application registered before this call is kept and runs after Ranetrace has captured the exception, so wiring Ranetrace in never takes over your own error page. When there was no previous handler, the exception is rethrown after capture, so PHP still prints its usual uncaught-exception report and exits non-zero, exactly as it would without the SDK installed.
- **A shutdown handler**, for fatal errors (`E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR`). These never reach an exception handler at all, so the last error of a dying process is inspected on the way out and reported as an `ErrorException`.

Calling `registerErrorHandlers()` more than once is harmless; the second call does nothing.

What it deliberately does **not** install is `set_error_handler()`. Notices, warnings and deprecations are not errors this SDK reports, and taking over the error handler would change how your own error reporting behaves for every one of them.

## Reporting manually

```php
try {
    $this->thirdPartyApi->sync();
} catch (Throwable $exception) {
    $ranetrace->report($exception);
}
```

`report()` takes a single `Throwable` and returns nothing. It never throws: losing one error report is acceptable, breaking your application is not. If the SDK cannot capture the exception, it writes why to its own diagnostics file and moves on.

Two things are skipped on purpose: exceptions thrown from inside the SDK itself (otherwise a transport failure would be reported as one of your application errors and loop straight back), and everything at all while `enabled`, `errors.enabled` or the API key is missing.

## What a report contains

- **The exception**: message, file, line, and the class name of the throwable.
- **The stack trace**, as PHP's own `getTraceAsString()` output.
- **A source preview**: the failing line plus five lines either side, dedented, along with which of those lines failed. Only for files that are readable and under 1 MB.
- **Request context**, when the process is not a CLI process: the full URL, the HTTP method, and the request headers.
- **Console context**, when it is: the command line the process was started with, and its arguments.
- **The current user**, when a `user_resolver` is configured.
- **Environment facts**: your environment name, the PHP version, and the framework name and version when you configured them.

File paths are reported relative to `project_root`, so your deployment layout does not travel with the report. A path outside the project root is kept absolute.

### Headers

Headers are captured from an allowlist, and everything else is masked as `***`. The allowlist is: `accept`, `accept-charset`, `accept-encoding`, `accept-language`, `cache-control`, `connection`, `content-length`, `content-type`, `host`, `referer`, `user-agent`, `x-requested-with`, `x-forwarded-proto` and `x-forwarded-host`.

Masking by default means a header carrying a secret nobody anticipated is masked rather than leaked. `x-forwarded-for` is deliberately not on the list: it carries the client IP chain, and no IP leaves your host. The `referer` value is additionally URL-scrubbed, because a referring URL can carry reset tokens and signed-URL signatures in its query string.

### The user

The SDK has no authentication system to ask, so it asks you. Configure a callable that returns the current user, or `null`:

```php
$ranetrace = Ranetrace::init([
    'key' => getenv('RANETRACE_KEY') ?: '',
    'user_resolver' => static function (): ?array {
        $user = Session::user();

        return $user === null ? null : ['id' => $user->id, 'email' => $user->email];
    },
]);
```

The resolver must return an array with an `id`, or `null`. It is called from inside the capture path, so if it throws, the failure is contained and the report is sent without a user rather than lost.

**The email is not sent by default.** `errors.capture_user_email` is `false`, so only the id travels. Turn it on when you want to see who hit an error:

```dotenv
RANETRACE_ERRORS_CAPTURE_USER_EMAIL=true
```

### Secret scrubbing

Before anything is buffered, values stored under sensitive keys and `key=value` secrets written into strings are redacted to `[REDACTED]`. The built-in key fragments are matched case-insensitively as substrings: `password`, `passwd`, `secret`, `token`, `api_key`, `apikey`, `api-key`, `authorization`, `credential`, `private_key`, `access_key` and `signature`. URL query parameters are scrubbed the same way.

The exception message and the stack trace are scrubbed **before** they are truncated, so a secret cannot survive by sitting across the length boundary.

Add your own key fragments, which are added to the built-in list and never replace it:

```php
$ranetrace = Ranetrace::init([
    'key' => getenv('RANETRACE_KEY') ?: '',
    'scrubbing' => ['extra_keys' => ['x_internal_signature']],
]);
```

Or through the environment, as a comma-separated list:

```dotenv
RANETRACE_SCRUBBING_EXTRA_KEYS=x_internal_signature,partner_ref
```

Scrubbing is defense in depth. Avoid deliberately putting secrets in exception messages regardless.

## When you get notified

Ranetrace groups every report with the same exception class, file, line and environment into one error, and each report in that group is an occurrence. An error notifies you:

- on its first occurrence, so a new error never goes unseen
- on its 5th and 10th occurrence, which tell a one-off from a pattern
- when it starts happening 10 times within an hour after a calmer hour, however old the error is
- when it comes back after you resolved it, even if it happens only once

An error that keeps failing at a steady rate notifies once when the burst starts, not every hour. It notifies again only after it has been quiet for an hour and then bursts again. A snoozed error stays silent until the snooze ends, and an ignored error is not recorded at all.

The same error with the same message is recorded at most once every 5 minutes, so the occurrence count measures how often an error comes back, not every single request it failed. It also means a burst of 10 within an hour takes about 45 minutes of steady failure.

Notifications land in your inbox and on the channels you connected. Turn error notifications off or on under **Notify**, **Rules**.

## Configuration

| Environment variable | Config key | Default | What it controls |
|---|---|---|---|
| `RANETRACE_ERRORS_ENABLED` | `errors.enabled` | `true` | Whether exceptions are captured at all. |
| `RANETRACE_ERRORS_TIMEOUT` | `errors.timeout` | `10` | Seconds to wait for the API when a batch of errors is sent. |
| `RANETRACE_ERRORS_CAPTURE_USER_EMAIL` | `errors.capture_user_email` | `false` | Whether the resolved user's email is included. |

To turn error tracking off without uninstalling:

```dotenv
RANETRACE_ERRORS_ENABLED=false
```

Captured errors reach the API on the next flush. See [PHP installation](/docs/php-installation) for the two ways that happens.
