Blog

Keeping PII out of exception messages before they ever leave your app

QueueHawk already treats job payloads conservatively: the actual argument values passed into a Hangfire job are never sent unless you explicitly opt in with IncludeJobPayloads. But a failed job doesn't just carry its arguments — it carries whatever the exception itself says, and an exception message is free-text written by whatever code (yours, a library's, a database driver's) happened to throw it. Occasionally that text contains something it shouldn't.

Where this actually shows up

A validation exception that quotes the value it rejected ("Invalid email: jane.doe@client.com"). A unique-constraint violation from the database driver that echoes the conflicting row's key. A third-party SDK that includes a request URL, complete with a query string, in its exception message. None of this is a bug in QueueHawk or in Hangfire — it's just how exceptions work: the message is whatever the throwing code decided to put there, and most code wasn't written with "this text may leave the process" in mind.

Job payloads and exception content are two different problems with two different defaults, on purpose: payloads are opt-in because there's rarely a diagnostic reason to see them at all. Exception messages and stack traces are sent by default (only length-truncated, via MaxStackTraceLength) because they're usually the entire reason you're monitoring in the first place — an alert with no exception detail isn't much of an alert.

Redacting exception content with OnBeforeSend

For the cases where exception text itself needs scrubbing, QueueHawk.Agent exposes an OnBeforeSend callback on QueueHawkOptions. It runs on every captured state-change event, before that event is even written to the agent's local in-memory buffer — a redacted field never sits in your process's memory unredacted, let alone crosses the network.

Program.cs
builder.Services.AddQueueHawk(options =>
{
    options.ApiKey = builder.Configuration["QueueHawk:ApiKey"];
    options.Environment = "Production";

    options.OnBeforeSend = jobEvent =>
    {
        // Only Failed events carry exception content — everything else passes through.
        if (jobEvent.ExceptionMessage is null)
        {
            return jobEvent;
        }

        return jobEvent with
        {
            ExceptionMessage = Regex.Replace(
                jobEvent.ExceptionMessage,
                @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
                "[REDACTED-EMAIL]"),
        };
    };
});

A few things worth knowing about how it behaves:

What about a plain Hangfire filter instead?

It's technically possible to write your own IElectStateFilter that mutates FailedState.Exception directly, registered with a lower filter order than QueueHawk.Agent's own filter so it runs first. The difference: that rewrites what Hangfire itself considers the exception for that job — including what your own team sees in the local Hangfire dashboard and in your application logs, not just what reaches QueueHawk. OnBeforeSend redacts only the copy QueueHawk sees, leaving your own local diagnostics untouched. For most teams that's the more useful default — you want your own engineers to see the real exception while it's fresh, and only strip sensitive detail from what leaves the process.

Payloads are still the bigger lever

If a job's exception messages routinely reference customer data, it's often a sign the underlying job design surfaces more than it needs to — worth a look regardless of what monitoring tool is involved. But for the cases where that's unavoidable or comes from a dependency you don't control, redaction at the agent boundary is the practical fix. Combined with IncludeJobPayloads staying off by default, the two options cover both places sensitive data can end up in a job event: the arguments, and the failure text.

Full option reference, including defaults and every other configuration knob: Agent configuration reference.

← All articles See the full configuration reference →

Monitoring that respects what you don't want it to see

Free for one application, no card required.

Start free