Back to blog

There Is a Customer's Email Address in Your Error Tracker Right Now

10 min read
On this page

Open your error tracker, pick any exception from the last week, and expand the stack frame where it was thrown. Look at the local variables.

Somewhere in there, on most applications we have looked at, is a real person's data. An email address in a $user object. A full request body from the checkout form that blew up, complete with a name and a delivery address. A session identifier. Occasionally a password, because somebody logged the request before validation stripped it.

Nobody decided to collect any of that. There was no data protection impact assessment for it, it does not appear in the privacy policy, and the person whose details are sitting in that frame has no idea it happened. It got there because an exception fired and your SDK helpfully captured everything in scope.

That is a GDPR problem, and it is a considerably more awkward one than the data residency question that usually dominates this conversation.

Why this is legally uncomfortable

Article 4(1) defines personal data as any information relating to an identified or identifiable natural person, whether identification happens directly or indirectly. An email address in a captured variable is squarely inside that definition. So is a user ID that maps to a customer record, and so is an IP address, which European regulators and the CJEU have treated as personal data for years.

The obvious escape hatch is pseudonymisation, and it does not work as well as people hope. Article 4(5) defines it as processing personal data so that it can no longer be attributed to a specific person without additional information held separately. That is a genuine security measure, and Article 32 explicitly names it as an example of one. But pseudonymised data remains personal data under Article 4(1), which the EDPB has restated more than once. Replacing a name with a customer number does not take the record outside GDPR. It reduces risk. It does not change the category.

Then there is the principle almost nobody applies to observability tooling. Article 5(1)(c) requires data minimisation: personal data must be adequate, relevant, and limited to what is necessary for the purpose. The purpose of your error tracker is diagnosing faults. A customer's full delivery address is not necessary for that purpose, and it is hard to argue otherwise with a straight face. Article 5(1)(e) adds storage limitation, meaning personal data should not be kept in identifiable form for longer than necessary, which is worth thinking about next to a 90-day retention setting you picked without much deliberation.

The transfer question sits on top of all that. Chapter V, Articles 44 through 49, governs sending personal data outside the EEA, and Schrems II established that controllers relying on standard contractual clauses must verify, transfer by transfer, whether the destination country's law actually provides essentially equivalent protection. The CJEU found US law did not, which is why supplementary technical measures became the standard advice rather than an optional extra.

For context on how live this is: cumulative GDPR fines reached €7.1 billion by January 2026, and the Austrian, French and Italian supervisory authorities have all issued decisions against US-based tools specifically on transatlantic transfer grounds.

The residency part is the easy part

Here is where we differ from most vendors writing about this, including how we could have framed it.

Data residency is largely a solved problem now, and it is solved cheaply. Sentry offers an EU region on every plan including the free tier, hosted in Germany, at no additional cost. GlitchTip runs an EU instance. AppSignal is a Dutch company hosting in the EU by default. If your only concern is "does this data sit on European soil," you can fix that this afternoon by choosing a region, and you do not need to switch vendors to do it.

There is a residual argument about corporate domicile, which we made in detail when we launched Tindra: a US-headquartered company operating a Frankfurt region remains subject to US law, which is why CISPE, the European cloud providers' association, has started calling the pattern "sovereignty washing." That argument is real and it matters to teams in regulated sectors. It is also not what this post is about, and we would rather not hide behind it.

Because even with perfect residency, even self-hosted on a machine in your own building, you are still storing personal data you never intended to collect, for a purpose that does not require it, for however long your retention window happens to be. Moving the server to Frankfurt does not fix Article 5(1)(c). It just changes the jurisdiction in which you are not complying with it.

What scrubbing actually catches, and what it does not

Every serious error tracker ships some form of PII scrubbing. Ours does too, and this is the part where we have to be honest about its limits, because the limits are the whole point of this post.

Tindra scrubs at ingest, so anything matching a rule never reaches the database at all. By default it filters fields named password, passwd, secret, api_key, token, auth, credentials, credit_card and ssn, matching case-insensitively and recursing through nested objects. It also runs pattern-based scrubbing across string values, with built-in patterns for email addresses and for IPv4 and IPv6 addresses. You can add your own field names and your own regular expressions.

That covers HTTP request bodies and query strings, custom extra data, breadcrumbs, user context apart from the id field, span data, and log messages captured as breadcrumbs.

Now the uncomfortable bit. It does not apply to stacktrace source code lines or file paths. That is documented, it is deliberate, and it means the specific thing this post is named after is the thing our server-side scrubbing does not solve.

We could have written this post without mentioning that. Most vendor content on this subject is constructed precisely to avoid mentioning it. But any competent engineer would find it in an afternoon of evaluation, and discovering it themselves after reading a post that implied otherwise would be worse for us than saying it plainly.

The reason for the gap is not laziness. Scrubbing a captured local variable inside a stack frame means understanding which values in a frame are sensitive without any field-name signal to go on, and doing it aggressively enough to be safe usually destroys the diagnostic value of the trace. A frame with every string replaced by [Filtered] will not help you at three in the morning. This is a genuinely hard problem and, as far as we can tell, nobody in this category has solved it well.

So scrub it before it leaves

The only reliable fix is to stop the data at the source, in the SDK, before it is ever transmitted. Server-side scrubbing is a safety net for what you missed, and it should be treated as exactly that rather than as your primary control.

Every Sentry-compatible SDK exposes a before_send hook that runs on the event immediately before transmission, which is your last chance to remove things.

// config/sentry.php
'before_send' => function (\Sentry\Event $event): ?\Sentry\Event {
    if ($request = $event->getRequest()) {
        $data = $request->getData();
        unset($data['password'], $data['card_number']);
        // rebuild the request without the sensitive fields
    }

    return $event;
},
Sentry.init({
  dsn: '...',
  beforeSend(event) {
    if (event.request?.data) {
      delete event.request.data.password;
    }
    return event;
  },
});

Two configuration decisions matter more than the hook itself. The first is whether you are sending request bodies at all, which most SDKs let you disable entirely, and which is the single highest-value change available to most teams.

The second is send_default_pii. It defaults to false in Sentry-protocol SDKs, and at that setting the SDK filters sensitive HTTP headers, sends no cookies, sends nothing about the logged-in user (no email, no user ID, no username), and does not send the user's IP address. Flipping it to true turns all of that back on at once, and somebody on your team has almost certainly done so at some point because it made a debugging session easier. Go and check what it is set to in production. In our experience it is enabled in precisely the applications where it should not be, and nobody remembers doing it.

Beyond that, the useful habits are unglamorous. Set retention deliberately rather than accepting a default, because storage limitation under Article 5(1)(e) is a decision you are making whether or not you realise it. Treat your error tracker as in-scope during data protection reviews, since it processes personal data and therefore needs to appear in your records of processing under Article 30. And if you use a hosted service, make sure you actually hold a signed data processing agreement with the provider under Article 28, because a processor relationship without one is a straightforward breach independent of anything else discussed here.

A pragmatic order of work

If this post has made you want to go and look at your own instance, the sequence we would follow is roughly this.

Start by reading real events rather than reasoning about them abstractly. Pull ten recent exceptions from your production project and read every frame properly. You are looking for the categories of data you did not expect, and there will be some.

Then turn off request body capture unless you have a specific reason to keep it, and verify send_default_pii is not enabled. Those two changes alone remove most of the exposure in most applications.

After that, add before_send filtering for the fields your application handles that a generic scrubber will never guess: your internal identifiers, your domain-specific sensitive fields, anything a regulator would consider special category data under Article 9.

Configure server-side scrubbing next, as a backstop rather than a solution, adding custom patterns for whatever formats are specific to you.

Finally, set retention to a number you can justify out loud, and write the error tracker into your processing records so that the next audit does not treat it as a surprise.

The disclaimer, and we mean it

We are engineers who run infrastructure for European clients, not lawyers. The articles cited here are quoted accurately and the interpretations are mainstream, but GDPR is applied by supervisory authorities and courts on facts we do not have, and your DPO or counsel is the person who should be making the call for your organisation. Take this as a technical description of a real risk that is worth raising with them, rather than as advice you can rely on.

What we are confident about is the engineering. Stack traces capture more than people think, most teams have never looked, and the fix is cheaper before the data leaves your application than after it lands in someone's database.

Where we come into it

We build Tindra, which handles error tracking, performance, uptime and cron monitoring from a single Go binary, self-hosted or managed by us in the EU. Managed instances run in dedicated containers with dedicated Postgres, with a signed DPA under Article 28 and data deletion within 24 hours. If you self-host it, none of this data leaves your building and the residency conversation ends there.

We also just told you, in the middle of a post about stack traces, that our stack trace scrubbing has a gap. That is because the honest version of this argument is more useful to you than the flattering one, and because you would have found out anyway.

If you want someone to go through your observability setup and tell you what it is actually collecting, that sits inside our server management and consulting work. Talk to us, the first thirty minutes are free, and we will tell you if the answer is "you are fine, change two settings."

Written by

Blendbyte

Blendbyte Team

We run what we write about. Production experience only, no theory.

Know what broke before your users tell you.

Tindra combines error tracking, performance monitoring, and cron monitoring in a single Go binary. It speaks every Sentry SDK, so you can switch without touching application code.

Check out Tindra