Field History Tracking keeps a permanent log of every change made to a tracked field, storing both the old value and the new value in a separate history object. When you mask a Contact's Email or a Lead's Phone after a sandbox refresh, the current record looks clean. The history object, sitting quietly behind it, often still holds the original real value untouched. That's the gap most masking projects never account for, and it's a straightforward compliance liability.
What Field History Tracking Actually Stores
Salesforce lets admins enable history tracking on up to 20 fields per object. Every time a tracked field changes, the platform writes a row to an object like ContactHistory or a custom __History object, capturing the field name, the old value, the new value, the user who made the change, and a timestamp. This data lives independently of the parent record.
That independence is the problem. Masking tools generally operate by updating the current field value on the parent record through standard DML or bulk API calls. Unless the masking process explicitly reaches into the history objects, those rows are never touched. The old value column keeps the real customer's original email address, phone number, or billing detail, sitting in a related list that any sandbox user with read access to History records can open.
Standard fields like Email, Phone, and MailingAddress are commonly tracked because support and sales teams want visibility into who changed what. Custom fields holding national ID numbers, loyalty account numbers, or internal risk scores get tracked just as often. None of it is exempt from GDPR just because it lives in a secondary object.
Why Masking the Current Record Isn't Enough
Consider a Contact record where Email was changed three times over two years before the account was flagged for a sandbox refresh. A masking tool that only updates the live Email field replaces the current value with a fake one. Fine, so far. But the ContactHistory object still contains three rows, each with a real email address from a real person, tied to a real change date.
A developer testing an email validation trigger in that sandbox can open the related history list on the Contact page layout and see the genuine data within seconds. No SOQL query required, no special permission beyond standard field-level access. This is precisely the kind of exposure Article 32 asks organizations to prevent through technical measures, and it happens inside a sandbox that was supposedly already masked.
The same issue applies to OpportunityFieldHistory, CaseHistory, and any custom history object generated for a tracked custom field. If your organization tracks Social Security Number equivalents, tax IDs, or health-related custom fields for audit purposes, the history object is arguably a bigger exposure than the current record, since it can contain multiple historical real values stacked up over years of edits.
Where This Actually Bites You
The exposure isn't theoretical. It shows up in three predictable places once a sandbox goes live for testing or demos.
- Related lists on page layouts. Any user profile with read access to the object's history related list sees old values by default, no code required.
- Reports built on history objects. Teams frequently build change-tracking reports for audit purposes. Run that report in a masked sandbox and the export contains real PII sitting next to fake current-record data, which is a confusing and dangerous mix.
- API and integration access. Any integration user or connected app with API access to History objects can query old values directly through REST or SOQL, bypassing the UI entirely. Demo environments connected to third-party tools inherit this risk automatically.
I've seen teams pass a full masking audit on every standard object, congratulate themselves, and then get flagged six months later because a support rep pulled up a Case's field history during a training session and read a customer's real address out loud on a recorded call. The masking process was technically thorough. It just stopped one object short.
How Masking Tools Should Handle History Objects
There are really three approaches to dealing with tracked history data, and each carries different tradeoffs.
| Approach | What it does | Tradeoff |
|---|---|---|
| Purge history rows | Deletes history entries for masked fields entirely after the refresh | Loses legitimate audit trail data useful for testing change-tracking logic |
| Mask old and new values in place | Rewrites the OldValue and NewValue columns with fake but structurally consistent data | Requires the masking tool to know which history objects map to which masked fields, adds processing time |
| Leave history untouched | Default behavior of most naive masking scripts | Leaves the exact PII the masking project was meant to eliminate, defeats the purpose |
Purging is the simplest and, in my view, usually the right default for sandboxes used for development and QA, since almost nobody is testing field-history business logic in a lower environment. But if your team specifically tests audit or compliance workflows against history data, purging breaks that testing. In that case, masking the history rows with values that match the fake current-record data is the better route, because it keeps referential logic intact without keeping real data around.
MaskEzee handles this by mapping tracked fields to their corresponding history objects automatically during the pre-refresh scan, then applying the same masking rule used on the parent field to the OldValue and NewValue columns in every historical row. A masked Email field produces consistently fake values in both the current record and every historical entry tied to it, so referential consistency holds even three edits deep.
Auditing Your Org for Tracked Fields Before the Next Refresh
Before you assume your masking coverage is complete, pull a list of every field with history tracking enabled across every object in your org. Setup does not surface this in one place by default, so most teams build it through Object Manager, checking each object's Fields & Relationships page for the tracking indicator, or by querying EntityDefinition and FieldDefinition metadata through the Tooling API.
Once you have that list, cross-reference it against your existing masking rule set. Any field flagged as containing PII in your masking configuration should have a matching entry confirming its history object is also covered. Gaps here are common specifically because masking projects are usually scoped by the admin team responsible for the parent object, while history tracking is often enabled years earlier by a completely different team solving a completely different problem.
Custom objects deserve particular attention. A custom Loan_Application__c object with history tracking enabled on a Loan_Amount__c or SSN__c field won't show up in generic PII scans that only check standard field labels like Email or Phone. Naming conventions vary too much across orgs for label-matching alone to catch everything, which is exactly why a masking tool needs configurable field-level rules rather than a fixed list of standard field names.
What to Do If You Find Gaps Right Now
If this article just made you check and you found real PII sitting in history objects in a sandbox your team already uses, don't wait for the next scheduled refresh to fix it. Run a targeted purge or mask on the affected history objects immediately, and audit who has had access to that sandbox since it was last refreshed, since anyone who opened those related lists has already seen the data.
Going forward, treat field history tracking as part of your masking scope from day one, not an afterthought discovered during an audit. Any field you decide to mask should trigger an automatic check for a corresponding history object, and that check should be a hard gate in your masking configuration rather than a manual step someone remembers to run.
Field History Tracking exists to give teams visibility into change management. It was never designed with masking in mind, and that mismatch is exactly why it keeps slipping through otherwise solid data protection programs. Close it once, build the check into your process, and it stops being a recurring finding on your compliance reports.
Frequently Asked Questions
Does Salesforce Field History Tracking data get masked automatically during a sandbox refresh?
No. A standard sandbox refresh copies field history data along with everything else, and most masking tools only update the current value on the parent record. Unless the masking tool explicitly maps tracked fields to their history objects, the old and new values stored in history rows remain untouched and can contain real PII.
Which Salesforce objects store field history data?
Standard objects use objects like ContactHistory, AccountHistory, OpportunityFieldHistory, and CaseHistory. Custom objects with history tracking enabled generate a corresponding custom history object, typically named with an __History suffix. Each stores the field name, old value, new value, the user who made the change, and a timestamp.
Should I purge history data or mask it during a sandbox refresh?
Purging is usually the simpler and safer default for development and QA sandboxes, since most teams are not testing field-history-specific logic there. If your team actively tests audit trail or change-tracking workflows, masking the old and new values consistently with the parent field's masked data preserves referential integrity without keeping real PII around.
How do I find which fields in my org have history tracking enabled?
Check each object's Fields & Relationships page in Object Manager for the tracking indicator, or query FieldDefinition and EntityDefinition metadata through the Tooling API to get a full list across every object at once. Cross-reference that list against your masking configuration to confirm every tracked PII field also has its corresponding history object covered.
Can users see real data in field history even if the current record is masked?
Yes, and this is the core risk. Any user profile with read access to an object's history related list can view old field values directly on the record page, no special permission or query needed. Reports and API queries against history objects expose the same real data even faster.