Skip to content

Error tracking

Error tracking is on by default (errors.enabled = true), but it captures nothing until you wire Ranetrace into Laravel's exception handler. That one line is required.

Register the exception handler

In bootstrap/app.php, call Ranetrace::handles() inside ->withExceptions():

use Illuminate\Foundation\Configuration\Exceptions;
use Ranetrace\Laravel\Facades\Ranetrace;

->withExceptions(function (Exceptions $exceptions) {
    Ranetrace::handles($exceptions);
})

Without this wiring, unhandled exceptions are not captured (though Ranetrace::report($exception) still works for in-flow calls). handles() preserves Laravel's own default logging. Once it is in place, anything that flows through Laravel's report(), including unhandled exceptions Laravel catches, reaches Ranetrace::report($exception) and is sent to Ranetrace after the worker runs.

What gets sent

From Ranetrace\Laravel\Ranetrace::report():

  • The exception: message, file, line, and the class name of the throwable.
  • Stack trace, in two forms: PHP's own text trace, and structured frames. Each frame is a file (relative to your project root), line, class, function and call type, with a flag that says whether the file is in vendor/. A trace keeps at most 50 frames, the 40 innermost and the 10 outermost, and says how many it left out in between.
  • Source lines around the line that threw and around your own frames: see Source lines below.
  • Previous exceptions: up to 5, nearest first, each with its class, message, the file and line it was thrown at, the source lines there, and its own frames.
  • HTTP context (when not running in console): full URL, HTTP method, and headers. Headers are captured from an allowlist (such as accept, content-type, host, referer and user-agent), and every other header, cookie and authorization among them, is masked.
  • Console context (when running in console): the command line invocation and argv.
  • Authenticated user (if any): the id, and the email only when RANETRACE_ERRORS_CAPTURE_USER_EMAIL=true.
  • PHP version and Laravel version.

Everything in the report is secret-scrubbed before it is buffered. A value under a key whose name holds one of password, passwd, pwd, secret, token, api_key, apikey, api-key, authorization, credential, private_key, access_key or signature (matched case-insensitively, anywhere in the name) arrives as [REDACTED]; add your own fragments under ranetrace.scrubbing.extra_keys. Two kinds of secret are also recognised by their value alone, wherever they stand in the message, the trace, a source line or an exception's context(): the password in a URL, with the scheme, user name and host kept (pgsql://app:secret@db/app arrives as pgsql://app:[REDACTED]@db/app), and a token whose prefix says it is a credential: Stripe sk_live_ and rk_live_ keys, AWS AKIA and ASIA access key ids, GitHub ghp_, gho_ and github_pat_ tokens, Slack xoxa-, xoxb-, xoxp- and xoxr- tokens, GitLab glpat- tokens and JWTs.

Source lines

Source lines are on by default. A report carries a few lines of your code: five either side of the line that threw, of each previous exception's throw site, and of each frame in your own code. Only files under 1 MB are read. That is what lets Ranetrace show the failing code next to the trace without access to your repository. It also means some of your source code leaves your application with every error, so decide whether you want that.

Every line is secret-scrubbed first, a window at a time together with up to 50 lines above it, so a secret that spans lines is caught as well: a value on the line after its key, a call spread over several lines, a heredoc assigned to a sensitive key and the body of a private key block. What arrives as [REDACTED]:

  • A value next to a sensitive key name, such as 'api_key' => '...', $password = '...', $config['password'] = '...' or env('STRIPE_SECRET', '...'). A quoted value goes whole, up to its closing quote. A value that is code keeps its code and loses only its string literals, so 'password' => Hash::make('x') arrives as 'password' => Hash::make('[REDACTED]'). A literal that only names where a value lives is kept: the first argument of env(), getenv() and config(), and the subscript of $_ENV and $_SERVER, so env('DB_PASSWORD', 'x') arrives as env('DB_PASSWORD', '[REDACTED]').
  • Every string literal passed to a call whose name holds a sensitive word, such as setApiKey('...'), Http::withToken('...') or ->setCredentials('user', 'pass'). A call whose name ends in Exception or Error stays readable, since its argument is a message the error already carries.
  • Every string literal passed to a known credential-taking call: new PDO(...), new mysqli(...), mysqli_connect, mysqli_real_connect, ldap_bind, ldap_sasl_bind, ftp_login, ssh2_auth_password, imap_open, Laravel's withBasicAuth and withDigestAuth, and parent::__construct(...) in a class that extends PDO or mysqli. So new PDO($dsn, 'root', 'hunter2') arrives as new PDO($dsn, '[REDACTED]', '[REDACTED]').
  • A URL password or a credential-prefixed token, wherever it stands, as everywhere else in the report.

Not recognised: a credential passed by position to a call on neither list, such as your own connect($host, 'root', 'hunter2'), and any other secret under a name the key list does not know, such as $pin = '4821'. Add such names to ranetrace.scrubbing.extra_keys, keep secrets in .env rather than in code, or turn source lines off as shown below.

Two things the structured frames never carry:

  • Argument values. The package does not read a frame's arguments at all. The text trace is PHP's own getTraceAsString(), which shows shortened argument values unless zend.exception_ignore_args is on (php.ini-production turns it on); it is secret-scrubbed like the message.
  • Source from your dependencies. Frames in vendor/ carry their file, line, class and function, but no lines. The one place vendor code can appear is a throw site: when an exception is thrown inside a dependency, the lines around that throw site are sent.

Blade views. An error inside a Blade template runs in a compiled file under storage/framework/views. A frame there is reported as the template you wrote instead: its .blade.php path, its line, and the lines around it, so the frame and the "Open in editor" link point at the template. The error's own file and line stay the compiled view's, so this does not split existing errors into new ones.

To keep every source line inside your application, set:

RANETRACE_ERRORS_CAPTURE_SOURCE=false

or ranetrace.errors.capture_source to false in a published config/ranetrace.php. With source capture off, no source line is sent at all, the throw site's included. The frames still travel, as file, line, class and function, so the trace reads the same without the code beside it.

Reporting manually

use Ranetrace\Laravel\Facades\Ranetrace;

Ranetrace::report($exception);

report() takes a single Throwable argument: there is no second context parameter.

You can also call Laravel's built-in report() helper; it routes through the same handler:

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

Testing your setup

php artisan ranetrace:test-errors

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

From config/ranetrace.php:

'errors' => [
    'enabled' => env('RANETRACE_ERRORS_ENABLED', true),
    'queue' => env('RANETRACE_ERRORS_QUEUE', true),
    'queue_name' => env('RANETRACE_ERRORS_QUEUE_NAME', null), // unset: the connection's default queue
    'timeout' => env('RANETRACE_ERRORS_TIMEOUT', 10),
    'capture_user_email' => env('RANETRACE_ERRORS_CAPTURE_USER_EMAIL', false),
    'capture_source' => env('RANETRACE_ERRORS_CAPTURE_SOURCE', true),
],

To disable without uninstalling:

RANETRACE_ERRORS_ENABLED=false