sblum/exception-enricher
A Laravel/PHP package that enriches exceptions with useful context, such as request, user, and application data, making logs and error reports more actionable. Add custom enrichment logic to improve debugging and observability.
This release is primarily a hardening release — the recording pipeline now treats request bodies as sensitive-by-default. If you already ship this package, the upgrade is behavior-changing rather than API-breaking: no method signatures changed, but what ends up in your logs does.
Requirements first: PHP 8.3+ is now mandatory. Check composer.json platform config and your CI matrix before bumping; a Laravel app pinned to PHP 8.2 will fail resolution.
Where to look first, in order:
request_post_data record field — this is the single place all four changes (level gating, truncation, masking, drop-on-unmaskable) converge.config/logging.php channel wiring — confirm which channels actually receive these records.request_post_data, since three of the four changes can make it disappear entirely.First use case: trigger a failing POST route and log at WARNING:
Route::post('/checkout', function (Request $request) {
try {
// ...
} catch (PaymentFailed $e) {
Log::warning('checkout failed', ['order' => $order->id]);
// record now carries request_post_data (masked, ≤2048 chars)
}
});
Confirm in the log that password/token fields are ***, that nested JSON is masked, and that a >2048-char body ends with … [truncated, N characters].
Log at WARNING or above when you need the body. INFO and DEBUG records still carry URI, user agent, IP, session and user — but no body. This is a deliberate cost/noise tradeoff, not a bug. If a diagnostic flow currently relies on Log::info() payloads, either promote the call or attach the body explicitly as context at the call site.
Name your fields for the masker. Masking is keyed on field names — password, token, jwt, secret, authorization, api_key, credential, plus any field name containing those substrings, at any nesting depth, including inside lists and objects. Structuring payloads as {"auth": {"api_key": "..."}} gets masked; {"auth": "..."} does not. This is the cheapest lever you have: shape your JSON so secrets sit under a recognized key.
Treat 2048 chars as a schema constraint. For large payloads, log a correlation identifier plus the handful of fields you actually need, rather than relying on truncation to save you:
Log::warning('webhook rejected', [
'payload_sha' => hash('sha256', $raw),
'payload_size' => strlen($raw),
]);
If you need full bodies for replay/debugging, that belongs in a dedicated store (DB column, object storage), not in the log channel.
Belt-and-braces redaction at the call site. The masker only covers recognized field names in JSON/urlencoded/form bodies. Values you interpolate into the message string, or fields outside that set (card_number, ssn, passwd), are untouched. Pass '***' explicitly for anything you know is sensitive:
Log::warning("charge failed for {$maskedPan}", ['gateway_response' => $safe]);
Test the masking, not just the logging. Assert on the emitted record in a feature test so a future release or config change can't silently reintroduce leakage:
Log::spy();
$this->post('/checkout', ['password' => 'hunter2', 'api_key' => 'sk_live_x']);
Log::shouldHaveReceived('warning')->withArgs(function ($msg, $ctx = []) {
return ! str_contains(json_encode($ctx), 'hunter2');
});
Integration tip for the Symfony bundle path: if you mount this bundle inside a Laravel app (or maintain a Symfony sibling service), note that services.yaml became services.php. Service definition overrides or compiler passes that assumed YAML-loaded definitions need re-verification, and the symfony/yaml dependency is no longer implicit — add it explicitly if your own code still parses YAML config.
Extension point: subclass the handler or register a custom processor after the masking step. Anything you inject before masking that re-serializes the body can bypass the redaction pass; anything after sees already-truncated, already-masked data.
Missing request_post_data is ambiguous. A record without the field means one of: level was INFO/DEBUG, the body was empty, or the body was deliberately dropped because it couldn't be masked. Don't write logic that treats absence as "no body was sent." If that distinction matters, add an explicit context flag at the call site.
Invalid JSON is dropped, not logged raw. Undecodable JSON — bad UTF-8, malformed structure — and unencodable JSON both cause the body to be omitted. BOM-prefixed JSON is handled. Test with your real clients' payloads, not hand-written fixtures; a mobile client sending a BOM-less Latin-1 body will look identical to "no body" in the logs.
XML and plain-text bodies containing a secret word are dropped entirely. There's no partial masking for these formats, and no fallback. If you have an XML API, expect those bodies to vanish the moment a field is named token — which for SOAP and SAML payloads is essentially always.
Substring matching cuts both ways. secretary_email and token_count get masked (annoying, loses diagnostics). pwd, passwd, pass, auth, key, card_number, ssn, dob do not (dangerous). The list is a starting point, not a compliance guarantee — pair it with application-level redaction and review what the masker actually emits for your domain objects.
Free text is never scrubbed. A password echoed into an exception message, a connection string inside a message field, a raw text/plain body without a matching field name — all pass through. Masking is structural, not semantic.
Truncation is applied post-masking. The … [truncated, N characters] suffix reports the full length, but the exact formatting is cosmetic and shouldn't be parsed by tooling. If you need the original size, log strlen() yourself.
PHP 8.3 is a hard floor. Run composer why-not php 8.3 (or check config.platform.php) before upgrading — transitive dev dependencies are the usual culprit, and CI will fail on the oldest matrix entry.
Symfony 8 : void signature fix. If you extend the bundle class, your build()/boot() overrides must now declare : void to match — a fatal error rather than a warning on PHP 8.3+.
PII surface from session/user capture. Every record — including DEBUG — carries IP, user agent, session and user identity. On a DEBUG channel in production that's a full behavioural trail with no body needed to be sensitive. Keep this handler on WARNING+ channels in production and disable it entirely on any channel you ship off-site.
Debugging checklist when nothing shows up: (1) is the log call at WARNING or above? (2) does the channel's minimum level let it through? (3) is the body non-empty and decodable? (4) does it contain a secret-word field name in an unmaskable format? Each of these fails silently, so add a one-off Log::warning('probe') from php artisan tinker to confirm wiring before investigating the payload.
How can I help you explore Laravel packages today?