Reports and dashboards don't store PII of their own. They read whatever sits in the underlying objects, so masking those objects should, in theory, take care of everything a report displays. In practice it doesn't, because Salesforce reports and dashboards masking has to account for snapshot objects, scheduled exports, cached images, and email subscriptions that hold onto real data long after the sandbox refresh finishes.
Admins who treat masking as a field-by-field exercise on Contact, Lead, and Account often stop there and call the job done. Then a report subscription fires the next morning with real customer names in a CSV attachment, sent from a sandbox that was supposedly clean. The object was masked. The report artifact wasn't.
Reports Don't Store Data, But They Do Cache It
A standard report runs live against object data at the moment someone opens it, which is why most teams assume masking is automatically inherited. That assumption holds for ad hoc reports run after a refresh completes. It breaks down for anything Salesforce persists as a separate artifact: report snapshots, dashboard component images, and exported files.
Dashboard components, for example, render from a cached image that refreshes on a schedule, not on every page load. If that cache was built before your masking job ran, viewers see the pre-mask numbers and names until the next refresh cycle fires. Depending on your dashboard refresh interval, that window can stretch for hours.
Report exports compound the problem. Anyone with export permission can pull a report to CSV or Excel at any point, and that file lives wherever it lands: a laptop, a shared drive, an email thread. Once PII leaves Salesforce in a spreadsheet, no sandbox-level masking tool can touch it.
Report Snapshots: A Second Copy Masking Never Touches
Report snapshots take a report's results and write them into a custom object on a schedule, usually for historical trending. That custom object is a full, independent copy of whatever fields the report included at the time it ran, and it persists after the source records change or get masked.
In my view, report snapshots are the single most overlooked leak vector in Salesforce masking programs. Teams mask Contact and Lead diligently, then forget that a snapshot object created six months ago still holds real emails, phone numbers, and deal values tied to those same records. The snapshot doesn't know the source got masked. It just sits there with its own copy of the truth.
If your masking scope is built purely from a schema scan of standard objects, custom snapshot objects will almost certainly get missed unless someone flags them explicitly. Any masking policy needs a step that inventories every object populated by a report snapshot job, not just the objects a report reads from.
Scheduled Exports and Email Subscriptions Bypass the Sandbox Entirely
Salesforce lets users subscribe to a report or dashboard and receive it by email on a schedule, no login required to see the data. That subscription runs against whatever is in the sandbox at send time, and it delivers the output straight to an inbox, completely outside Salesforce's access controls.
This matters because a masking job that runs after a refresh but before the next scheduled subscription still leaves a gap: the subscription that fired between refresh and mask completion carried real data. Order matters here as much as coverage does.
| Artifact | Where real data can survive masking | Fix |
|---|---|---|
| Report snapshot object | Historical rows written before masking ran | Include snapshot objects in scope, mask retroactively |
| Dashboard cache | Image rendered before refresh cycle | Force dashboard refresh after masking completes |
| Email subscription | Sent between sandbox refresh and mask job finishing | Pause subscriptions during the refresh window |
| Exported CSV/Excel | Downloaded to a local machine or shared drive | Restrict export permission in non-prod, or mask before granting sandbox access |
Dashboard Filters and Drill-Downs Expose Row-Level Detail
Dashboards look like aggregate views: charts, gauges, summary numbers. That's exactly why teams assume they carry lower PII risk than a raw list view. But most dashboard components allow a drill-down or a filter click that jumps straight to the underlying report, and from there to individual records.
A pipeline dashboard grouped by sales rep looks harmless until someone clicks a bar and lands on a filtered report showing opportunity names, contact emails, and deal amounts for that rep's book. If the sandbox wasn't masked before that dashboard went live for QA testers, the aggregate view was never the real exposure point. The click-through was.
Testing dashboards in a sandbox for layout and performance doesn't require real names behind the bars. It requires believable ones. Masked data that preserves realistic distributions, so a sales-by-region dashboard still looks like a functioning business, does the job without the liability.
Aggregate Fields Can Reconstruct Individual Records
There's a subtler risk in grouped and summary reports: aggregation doesn't guarantee anonymity. A report grouped by postal code with a COUNT of 1 in a given group effectively identifies a single record, and if other fields in that row (industry, account name, deal size) are visible, the aggregation adds nothing.
Small group sizes are common in sandboxes that use a partial copy or a filtered subset of production data, exactly the environments most teams use for QA and UAT. The smaller the dataset, the more likely a group-by report resolves to one identifiable person or account.
This is an argument for masking before subsetting, not after. If you mask the full dataset first and then take a partial copy, every remaining record is already fake. Subset first and mask second, and you're relying on the masking job to catch a smaller, sometimes oddly distributed dataset where edge cases are more likely to slip through.
What Correct Masking Order Looks Like for Reports
Getting reports and dashboards genuinely clean takes more than running a masking job against Contact and Account and calling it finished. It takes a sequence that accounts for every place report data can persist outside the live object.
- Inventory custom objects populated by report snapshots and include them in masking scope, not just the source objects.
- Pause or delay scheduled report subscriptions and dashboard refreshes until masking finishes, so no email or cached image goes out with real data.
- Mask before subsetting when building partial-copy sandboxes, so small group sizes in aggregate reports never expose a single identifiable record.
- Restrict report export permissions in sandbox profiles, since a downloaded CSV outlives any masking policy the moment it leaves Salesforce.
- Force a manual dashboard refresh immediately after the masking job completes, rather than waiting for the next scheduled cache update.
MaskEzee runs as a pre-refresh step precisely because sequencing is the hard part, not the field-level substitution. It masks source data before it ever lands in a sandbox, which means report snapshots created afterward, dashboards built on top of that sandbox, and any subscription that fires post-refresh are all working from fake data from the start. That closes the gap that field-by-field masking tools, applied after the fact, tend to leave open.
None of this is exotic. It's the kind of gap that shows up in an audit finding, not in a demo. Treat reports and dashboards as their own masking surface, with their own artifacts and their own timing risks, and the sandbox stops leaking PII through the one interface everyone actually looks at.
Frequently Asked Questions
Do report snapshots need separate masking after a sandbox refresh?
Yes. Report snapshots write results into a custom object on a schedule, and that object keeps its own copy of the data independent of the source records. Masking Contact or Lead after the fact does not touch historical snapshot rows created before the mask ran, so those objects need to be included explicitly in masking scope.
Can dashboard components expose PII even if the underlying report is masked?
Yes, in two ways. Dashboard components render from a cached image that only updates on its own refresh schedule, so a stale cache can show pre-mask data even after the report itself is clean. Drill-down clicks on chart components can also jump straight to a filtered report showing row-level detail, bypassing the aggregate view entirely.
What happens to scheduled report emails after a sandbox refresh?
Scheduled report and dashboard subscriptions run against whatever data exists in the sandbox at send time and deliver it by email, outside of Salesforce's normal access controls. If a subscription fires between the sandbox refresh and the completion of a masking job, it can send real PII to every subscriber's inbox before anyone notices.
Do aggregate functions like SUM or COUNT protect PII in grouped reports?
Not reliably. If a grouped report has a small group size, such as a COUNT of one for a given postal code or industry segment, the aggregation effectively identifies a single record. This is especially common in partial-copy or filtered sandboxes where the overall dataset is already small.
How does MaskEzee handle report snapshot objects?
MaskEzee masks source data before it enters the sandbox, so any report snapshot objects populated after the refresh are built from already-fake data rather than requiring a separate cleanup pass. It also supports scoping custom objects explicitly, which covers snapshot targets that a generic schema scan might otherwise miss.