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

Automatic capture

$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

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:

$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:

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:

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

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

RANETRACE_SCRUBBING_EXTRA_KEYS=x_internal_signature,partner_ref

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

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:

RANETRACE_ERRORS_ENABLED=false

Captured errors reach the API on the next flush. See PHP installation for the two ways that happens.