How to mask PII in session replays without breaking debugging

Block-list vs allow-list strategies, the per-attribute exceptions you'll forget about, and the masking tests we run on every release.

GlitchReplay team··
replayprivacy

You finally reproduce that "unhandled exception" in a session replay, only to find the entire screen is a series of gray boxes. The security team won the battle for PII protection, but you've lost the war against the bug. This "Blackout Effect" is the silent killer of session replay utility, and it usually happens because teams choose the "safest" global toggle rather than a granular masking strategy.

As developers, we're caught in a pincer movement. On one side, GDPR, CCPA, and your CISO demand that sensitive user data never touches our observability stack. On the other, we can't fix what we can't see. If every input, header, and text node is obscured, we lose the context of the user's journey. We can't see the validation error that didn't trigger an alert, or the confusing UI layout that led to a rage-click. Privacy compliance doesn't have to mean "blind debugging." You can have both, but it requires moving beyond basic toggles and implementing a tiered privacy strategy.

The "Blackout Effect" and the cost of over-masking

The easiest way to satisfy a security audit is to enable maskAllText: true and maskAllInputs: true. In your SDK configuration, this is a two-line change that effectively turns your session replay into a high-fidelity recording of a ghost ship. You see the mouse move, you see boxes change size, but you have no idea what the application actually said to the user.

Why global masking fails the "debuggability" test

When you mask everything, you lose the "Why." Imagine a checkout form where a user is stuck. With global masking, you see them click "Submit" five times. You see a red box appear at the top of the screen. Because that red box contains text, it is masked. Is the error "Credit Card Expired" or is it "Internal Server Error"? Without that string data, you're forced to cross-reference timestamps with server logs, defeating the primary purpose of session replay: immediate visual context.

Over-masking also creates a false sense of security while introducing "debugging debt." Developers start ignoring replays because they're "unreadable," leading to longer Mean Time to Resolution (MTTR) and higher frustration. The goal should be to mask the data, not the interface.

The risk of "Leaky Abstractions"

PII is a shapeshifter. It hides in places your global toggles might not reach. We've seen cases where a "mask all" policy was in place, yet user emails leaked through title attributes on gravatar images, or phone numbers appeared in ARIA labels for accessibility. Even worse is the "context leak": a URL parameter like ?email=user@example.com remains visible in the breadcrumbs even if the page content is perfectly masked. A robust strategy recognizes that masking is not a "set and forget" feature; it is an architectural requirement that needs to evolve with your UI.

Strategy 1: The "Sanity vs. Safety" Trade-off (Block-list vs. Allow-list)

When you decide to move away from "mask everything," you have two main paths. The choice depends on your team's velocity and your organization's risk tolerance.

Block-listing: Fast to implement, high risk of "PII slip"

Block-listing is the process of identifying specific elements or classes that contain sensitive data and telling the recorder to ignore them. For example, you might add .mask-me to your credit card inputs and address fields. It's fast and keeps the rest of the UI visible. However, it relies on developers remembering to add those classes every time they build a new feature. In a fast-moving codebase, "PII slip" is inevitable. A new "Direct Deposit" form gets shipped on a Friday afternoon, the dev forgets the masking class, and by Monday, your replay database is full of bank account numbers.

Allow-listing: The "Zero Trust" approach

Allow-listing (or "unmasking") is the inverted model. You mask everything by default and explicitly "unmask" structural components that you know are safe. This is the "Zero Trust" model for observability. You might unmask all <button> text, <h1> headers (assuming they don't contain user names), and navigation links. This ensures that even if a developer adds a new sensitive field, it is masked by default. The trade-off is maintenance; your replays might look broken until you've put in the work to unmask the safe parts of your layout.

The Hybrid Model

The most effective teams use a hybrid approach. They enable maskAllInputs (because inputs are almost always PII) but use a permissive unmask list for the structural CSS framework. This way, the "bones" of the site—buttons, navbars, sidebars—are always visible, giving you the necessary context to understand user flow without ever touching the sensitive string data in the middle of the page.

The "Forgotten" Attributes: Where PII hides in plain sight

Standard masking rules often focus on innerText and value. But if you're only looking at those, you're leaving the door wide open. Here are the technical edge cases that catch teams off guard.

Attribute-level PII

HTML attributes are a goldmine for accidental leaks. Consider a simple user profile icon:

<img src="/avatars/123.jpg" alt="Profile picture for sean@glitchreplay.com" title="sean@glitchreplay.com" />

Even if you mask the entire text content of the page, the alt and title attributes are often captured by DOM recorders as-is. You need to ensure your masking configuration explicitly targets these attributes or that your recorder is configured to ignore them on specific elements.

Dynamic content vs. Static UI text

There is a difference between "User Generated Content" (UGC) and "Application UI." A search bar is UGC; the "Search" button label is UI. Your masking strategy should treat them differently. We recommend a "structural integrity" approach: if the text comes from your translation files (i18n), it's likely safe to unmask. If it comes from a database field that a user can edit, it must stay masked.

Visual PII

Sometimes PII isn't text. User avatars, uploaded thumbnails, or even custom signature canvases are all forms of PII. If your application handles sensitive documents (like healthcare or legal forms), a "gray box" approach for images is non-negotiable. Most session replay tools allow you to block specific tags; ensure <img>, <canvas>, and <video> are on your high-sensitivity list for pages that handle private media.

Implementation: Sentry-SDK Compatible Masking

Since GlitchReplay is fully compatible with the Sentry SDK, you can use the standard patterns you already know. The key is to be surgical with your Sentry.init call. Don't just accept the defaults.

Configuring granular masking

Here is a TypeScript configuration that implements the "Hybrid Model" we discussed. It masks all inputs and text by default but unmasks common structural elements to keep the replay useful.

import * as Sentry from "@sentry/react";

Sentry.init({
  dsn: "your-glitchreplay-dsn",
  integrations: [
    Sentry.replayIntegration({
      // Mask everything by default
      maskAllText: true,
      maskAllInputs: true,
      
      // Explicitly allow-list structural UI elements
      unmask: [
        "nav", 
        "header", 
        "footer", 
        ".btn", 
        ".sidebar-link", 
        "h1", 
        "h2"
      ],
      
      // Explicitly block high-risk areas even if they match unmask rules
      block: [
        ".user-profile-card",
        ".billing-history-table",
        "[data-private]"
      ],
    }),
  ],
});

Scrubbing Breadcrumbs and Network Requests

Replays aren't just about the DOM. They also include breadcrumbs (logs, clicks) and network request metadata. If a user types their password into a "Username" field by mistake, and your app sends that in a POST request, it might end up in your telemetry. Use the beforeSend or beforeSendReplay hooks to scrub these.

Sentry.init({
  // ... other config
  beforeSend(event) {
    if (event.request && event.request.headers) {
      // Remove sensitive headers from every event
      const sensitiveHeaders = ['cookie', 'authorization', 'x-api-key'];
      sensitiveHeaders.forEach(h => delete event.request.headers[h]);
    }
    return event;
  },
});

Server-Side Scrubbing: The Final Safety Net

Client-side masking is your first line of defense, but it shouldn't be your last. Browsers are unpredictable. Sometimes a script fails to load, or a race condition allows a single frame of unmasked data to be captured before the SDK initializes. This is where server-side scrubbing comes in.

Regex-based scrubbing at the ingestion layer

GlitchReplay utilizes a Cloudflare-native architecture, meaning we can run scrubbing logic at the Edge—closer to your users and before the data ever touches our persistent storage. We use high-performance regex patterns to identify and redact common PII formats like Credit Card numbers (Luhn algorithm), Social Security Numbers, and API keys. If the client leaks it, the Edge catches it.

Handling PII in "Error Envelopes" vs. "Replay Segments"

It's important to distinguish between the Error Envelope (the JSON payload describing the crash) and the Replay Segment (the binary/compressed DOM recording). Many tools only scrub the JSON. GlitchReplay treats both as high-risk. By scrubbing the raw replay segments during ingestion, we ensure that even if a "forbidden" string was recorded by the browser, it is replaced with [REDACTED] before it is written to disk.

The Masking Test Suite: Verification for Every Release

If you don't test your masking, you don't have masking. You have a hope. We recommend a three-step verification workflow for SREs and Frontend Leads to run before every major production deploy.

1. Visual Diffing in Staging

Record a session in your staging environment while performing a standard "PII-heavy" flow (e.g., updating a profile, adding a credit card). Watch the replay. If you can read your own email or see your dummy credit card number, your rules are too permissive. It sounds manual, but 5 minutes of visual inspection can prevent a massive compliance headache later.

2. Automated scanning for high-entropy strings

For larger teams, you can automate this by querying your telemetry for high-entropy strings. PII often has a specific "randomness" profile. If you see strings in your replay payload that look like hashes or structured identifiers (and aren't on your allow-list), it's a sign of a leak.

3. The "PII Persona" Test

Create a dedicated QA user with extremely distinct PII—something like "SECRET_IDENTIFIER_99" for the name and "leak-test@example.com" for the email. After running your automated test suite, grep your ingestion logs (or use your tool's search function) for those specific strings. If they appear anywhere in your replay database, you know exactly which component is leaking.

Conclusion: Towards "Privacy by Design" in Observability

The industry is moving away from "record everything and apologize later" towards a proactive "Privacy by Design" approach. This doesn't mean we have to sacrifice our ability to debug. By using a tiered strategy—global input masking, structural unmasking, and server-side scrubbing—you can build a system that respects user privacy while giving developers the high-fidelity context they need to ship stable code.

At GlitchReplay, we believe transparency builds trust. When your users know that your session replays are strategically masked and that you're using a tool with a flat-rate pricing model that doesn't incentivize "sampling" (which often misses the very errors you need to see), you create a better relationship with your community. You don't have to choose between a happy CISO and a productive dev team. You just need a better masking strategy.

Stop choosing between privacy and visibility. Use GlitchReplay's Sentry-compatible SDK to implement robust PII masking on a flat-rate plan that doesn't punish you for being thorough.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.