Last month, I was working on a client’s React application—a fairly typical dashboard for managing customer information in a marketing services product—when we discovered something alarming. Personal customer data was silently making its way into our Sentry error logs. This discovery kicked off an urgent investigation that revealed some non-obvious ways React applications can leak sensitive data to monitoring tools. Sharing this experience because the it could happen to any team, yet serious enough that it deserves attention.

Note: The code snippets in this article are simplified for illustration. The actual production code contained complex business logic, and the identifiers that were logging PII were not immediately obvious in our codebase. The examples here have been distilled to clearly demonstrate the issue.

The Setup: Our Monitoring Infrastructure

Like many modern web applications, this dashboard used React (v17) on the frontend with a Node.js backend. We had integrated Sentry for error monitoring early in development—a standard practice to catch issues in production environments.

Our initial Sentry configuration looked something like this:

import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';

Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0000001',
  integrations: [new Integrations.BrowserTracing()],
  tracesSampleRate: 0.1,
  // We relied on Sentry's default PII scrubbing
  // plus some basic customization
  beforeSend(event) {
    // Basic custom scrubbing for our specific app patterns
    if (event.request && event.request.url) {
      event.request.url = event.request.url.replace(/\/users\/\d+/, '/users/[REDACTED]');
    }

    // ...other scrubbings
    
    return event;
  }
});

We were relying heavily on Sentry’s built-in PII scrubbing capabilities, which by default can handle things like passwords, credit cards, and social security numbers. We had also added some custom URL pattern scrubbing specific to our application. According to Sentry’s documentation, this approach could have been sufficient for basic PII protection.

The Unsettling Discovery

The issue came to light during a routine compliance review. The data protection officer was examining various systems for GDPR compliance when they asked to show them exactly what data was being sent to third parties. While reviewing Sentry logs together, we spotted customer email addresses and partial account numbers appearing in several error reports.

Further investigation revealed personal data appearing in three distinct locations within our Sentry events:

  1. In the React error boundary extra context data
  2. In the breadcrumbs automatically captured by Sentry
  3. In serialized Redux state snapshots added to error reports

The Investigation: Tracking Down the Source

The first step was to understand how this information was ending up in our error logs despite Sentry’s built-in protections. I started by examining recent errors in our dashboard that contained PII.

One pattern quickly emerged: most leaks were happening inside custom error boundaries we had implemented around critical components. These error boundaries were designed to gracefully handle failures but were inadvertently capturing and forwarding sensitive context data.

Here’s what one of our error boundaries looked like:

class ProfileErrorBoundary extends React.Component {
  state = { hasError: false };
  
  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    
    // Here's where the problem was happening
    Sentry.withScope(scope => {
      // We were adding the entire error context
      scope.setExtra("componentStack", info.componentStack);
      
      // This was the real issue - capturing state that contained PII
      scope.setExtra("componentState", this.state);
      
      // And sometimes even passing along props with sensitive data
      scope.setContext("componentProps", this.props);
      
      Sentry.captureException(error);
    });
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

This error boundary was wrapping components that handled customer profile data, so this.props often contained full customer objects with email addresses, account details, and other sensitive information.

The issue became clearer: Sentry’s default scrubbing was looking for known patterns in standard event fields, but our custom error boundaries were adding PII under custom attributes that bypassed the automatic scrubbing.

The Root Cause: Multiple Vectors for Data Leakage

After more digging, I found several interconnected issues:

1. Custom Error Context in Error Boundaries

Our error boundaries were capturing too much context, including props and state containing sensitive customer data. When errors occurred, this entire context was being sent to Sentry.

2. Redux Middleware Capturing State

We were using a custom Redux middleware that would capture state slices for debugging:

const sentryReduxMiddleware = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    // Capturing the entire profile slice of state - bad idea!
    const currentState = store.getState();
    Sentry.captureException(err, {
      extra: {
        action: action,
        state: currentState.userProfile // Contains PII!
      }
    });
    throw err;
  }
};

3. Detailed Console Logging Captured as Breadcrumbs

Throughout our application, developers had added detailed logging that sometimes included PII:

console.log("Loaded profile for user:", user.email, user.accountDetails);

Sentry’s automatic breadcrumb collection was picking up these console logs, complete with the sensitive data we were logging.

Why Sentry’s Built-in Scrubbing Wasn’t Catching This

Sentry does provide built-in data scrubbing capabilities as documented in their guides, but there are limitations:

  1. Default scrubbing primarily targets common patterns like credit card numbers and social security numbers
  2. It focuses on standard fields like cookies, headers, and query strings
  3. Custom context data added via setExtra and setContext often bypasses default scrubbing
  4. Breadcrumbs with console logs aren’t deeply scrubbed by default
  5. The server-side Advanced Data Scrubbing feature requires explicit configuration with custom rules

Our issues fell through these gaps - we were adding PII in custom contexts, breadcrumbs, and extra fields that weren’t covered by the default scrubbing patterns.

The Solution: Multi-layered PII Protection

Fixing this issue required multiple approaches:

1. Revise Error Boundaries to Limit Context

We updated our error boundaries to avoid capturing sensitive props and state:

componentDidCatch(error, info) {
  this.setState({ hasError: true });
  
  Sentry.withScope(scope => {
    // Only include the component stack, not state or props
    scope.setExtra("componentStack", info.componentStack);
    
    // If we absolutely need some context, explicitly scrub it first
    if (this.props.userProfile) {
      const safeProfile = {
        hasData: !!this.props.userProfile,
        // Only include non-PII fields we need for debugging
        sections: Object.keys(this.props.userProfile)
      };
      scope.setContext("profile", safeProfile);
    }
    
    Sentry.captureException(error);
  });
}

2. Configure Sentry’s Advanced Data Scrubbing

We implemented more thorough server-side scrubbing rules in Sentry’s dashboard:

Sentry Advanced Data Scrubbing

We added custom rules to target our specific patterns, including email addresses in custom contexts and account numbers in error messages.

3. Fix Redux Middleware

We rewrote our Redux error reporting middleware to be PII-aware:

const sentryReduxMiddleware = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    // Only capture the action type and safe metadata
    const sanitizedAction = {
      type: action.type,
      hasPayload: !!action.payload
    };
    
    // Avoid capturing sensitive state slices entirely
    Sentry.captureException(err, {
      extra: {
        action: sanitizedAction,
        // Only include safe state information
        stateKeys: Object.keys(store.getState())
      }
    });
    throw err;
  }
};

4. Improve Logging Practices

We implemented a custom logger that automatically redacts sensitive information:

const safeLog = (message, ...args) => {
  const safeArgs = args.map(arg => {
    if (typeof arg === 'object' && arg !== null) {
      // Create shallow copy to avoid mutating the original
      const safeCopy = {...arg};
      
      // Redact known PII fields
      ['email', 'password', 'ssn', 'accountNumber'].forEach(field => {
        if (field in safeCopy) safeCopy[field] = '[REDACTED]';
      });
      return safeCopy;
    }
    return arg;
  });
  
  console.log(message, ...safeArgs);
};

// Usage
safeLog("Loaded profile for user:", user); // Will redact sensitive fields

Lessons Learned: Privacy by Design

This experience taught me several valuable lessons about handling PII in frontend applications:

  1. Don’t trust default scrubbing alone. While Sentry offers good baseline protection, custom application needs require custom scrubbing rules.

  2. Be particularly careful with custom contexts. Error reporting middleware, error boundaries, and custom logging are common paths for PII leakage.

  3. Audit your breadcrumbs. Console logs captured as breadcrumbs are easy to overlook but can contain a wealth of sensitive information.

  4. Test your error monitoring in production-like conditions. Only by simulating real errors with real (or realistic) data could we have caught this issue earlier.

  5. Use server-side scrubbing as your last line of defense. Client-side prevention is good, but server-side rules ensure that even if PII slips through, it won’t be stored.

Moving Forward

As applications become more complex and privacy regulations more stringent, we need to approach error monitoring with the same care we apply to other aspects of security. The tradeoff between detailed error information and privacy protection isn’t always easy to navigate, but awareness of these potential issues is the first step.

For the application, the team has now implemented a periodical “monitoring audit” where we intentionally trigger various error conditions and examine what data is captured by our monitoring tools. This has already helped us catch several other minor issues before they became problems.

The key takeaway? Even with a robust tool like Sentry that offers built-in PII protection, the complexity of modern React applications creates numerous opportunities for sensitive data to leak through. A defense-in-depth approach is essential, with protection at both the client and server levels.