spatie/flare-debug-sender
A debug sender for Flare payloads, intended mainly for internal testing. It lets Laravel apps route error, trace, and Zipkin payloads through a configurable channel, with options to pass through data, alter tracing IDs and times, print payload details, and customize curl sender settings.
Install as a dev dependency and publish the Flare config so you have a sender key to override:
composer require --dev spatie/flare-debug-sender
php artisan vendor:publish --tag=flare-config
Where to look first: the entire package is one config array. Read the Options list in the README top to bottom, then open vendor/spatie/flare-debug-sender/src/Channels/ — the three channels (RayDebugChannel, LaravelLogDebugChannel, FileDebugChannel) tell you more about real usage than the docs do. Check the installed Flare SDK version first: 2.x of this package only exists to follow the Flare v3 packages, so on an older SDK you want 1.x.
Minimal, Ray-free starting point — write to a log file, send nothing to Flare:
// config/flare.php
'sender' => [
'class' => \Spatie\FlareDebugSender\FlareDebugSender::class,
'config' => [
'passthrough_errors' => false,
'passthrough_traces' => false,
'passthrough_zipkin' => false,
'channel' => \Spatie\FlareDebugSender\Channels\FileDebugChannel::class,
'channel_config' => [
'file' => storage_path('logs/flare-debug.log'),
],
'print_endpoint' => true,
],
],
Be explicit about every flag rather than relying on defaults — only channel (RayDebugChannel) and sender (CurlSender) have documented defaults, and you do not want an accidental passthrough.
First use case — verify an exception's context without a browser. You don't need a failing request; report the exception directly:
php artisan tinker
>>> report(new App\Exceptions\OrderSyncFailed($order));
Then tail -f storage/logs/flare-debug.log. You now see the exact payload Flare would have received: exception class, message, stack, the context fields you attached, the request/user/resource blocks, and the endpoint. That's the loop — trigger, read, delete the sender block, move on.
Gate it behind an env flag, not a committed config. The safest shape is to only inject the sender key when explicitly enabled locally, so the class name never appears in a cached production config:
// config/flare.php
'sender' => env('FLARE_DEBUG_SENDER')
? [ /* the debug sender array */ ]
: null,
Put FLARE_DEBUG_SENDER=true in your local .env only. Never true in .env.example.
Choose your channel by team, not by default. If the team already runs Ray, RayDebugChannel is the fastest feedback loop. If not, don't make Ray a prerequisite for debugging — use FileDebugChannel with an explicit path (or LaravelLogDebugChannel if you route it to a dedicated log channel rather than the default stack).
Passthrough is the main dial. The three passthrough_* flags decide whether the payload also goes to the real Flare/Zipkin. Combinations worth memorizing:
| Goal | Setting |
|---|---|
| Inspect payload, still get real reports | passthrough_errors => true |
| Full suppression, no outbound traffic | all three false |
| Trace/span debugging only | passthrough_errors => false, passthrough_traces => true |
Verify payload-mutating code. If you have middleware, a report() override, or a custom exception handler that rewrites context, this is how you prove the rewrite lands. Capture the payload once with the handler disabled, once with it enabled, and diff the two files — far more convincing than reading the handler source.
Upgrade gate for Flare majors. Before and after bumping Flare, run the same failing request through this sender and diff the captured payloads. Set replace_tracing_ids and replace_tracing_times to true for both runs so the IDs/timestamps don't drown the signal. Treat "readable" as readable — capture twice and confirm the output is actually stable before you trust a clean diff.
Slice instead of scrolling. print_full_payload => true produces a lot. Redirect it and query it:
tail -f storage/logs/flare-debug.log | jq '.context' 2>/dev/null
# or
rg -n '"user_id"|"headers"|"session"' storage/logs/flare-debug.log
Start with print_endpoint and print_resource alone; they're the cheap, low-noise toggles. Add print_full_payload only when you need the whole envelope.
Onboarding demo. The single highest-leverage non-debug use: show a new engineer a real payload instead of the Flare dashboard abstraction. Five minutes and they understand what the team is actually paying for.
Extension points, used sparingly. channel and sender are both swappable. A custom channel is the right hook if you want output somewhere the three built-ins don't cover (a Slack webhook locally, an in-memory buffer for a targeted assertion). The nested sender (default CurlSender) is where you'd intercept below the payload layer — note the changelog entry "Allow defining an alternative sender" is exactly this seam.
passthrough_errors is false in a shared environment, real exceptions stop reaching Flare and nobody notices until an incident. Env-gate it, keep it out of staging, and never copy the block into config/flare.php unconditionally.CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_SSL_VERIFYPEER => 0). Fine for a throwaway local endpoint or a curl proxy; do not let that pattern propagate into shared config. Drop those options entirely unless you actually need them.composer require --dev + production config cache is a trap. If the sender class name ends up in a config cached on a host that doesn't have the dev dependency installed, Flare fails to instantiate the sender. The env-guard above prevents this — verify by running php artisan config:cache and inspecting bootstrap/cache/config.php for the class name before deploying.FLARE_DEBUG_SENDER does nothing while a stale config cache exists. php artisan config:clear is the first thing to try when the sender "isn't working."FileDebugChannel's default path is flare-debug-sender.log in the project root. Set an explicit path under storage/logs/ and add the file to .gitignore — an accidentally committed payload log is a data-leak waiting to happen.LaravelLogDebugChannel writes into your normal log stream. It will interleave with application logs and can make tail useless. Either route it to a dedicated channel in config/logging.php or prefer the file channel.print_full_payload includes exactly the fields you'd worry about in a privacy review — user IDs, request bodies, headers, session data. That makes it excellent evidence for a data-handling sign-off, and also means the resulting log file is sensitive. Treat it like a dump of production data.dispatchSync() or QUEUE_CONNECTION=sync locally.report() and an uncaught throw produce slightly different payloads. If you're verifying the shape of what Flare receives in production, reproduce with an actual uncaught exception at least once, not just a tinker report().Spatie\FlareDebugSender\* into your own application code or test suite, and assume the sender config contract shifts with each Flare major. Check the changelog before upgrading Flare, not after.How can I help you explore Laravel packages today?