Storage · DOM Snapshot
Opt-in capture of localStorage, cookies, and DOM — with client-side masking of passwords and tokens.
Table of Contents
Capture localStorage, sessionStorage, cookies, and the DOM outerHTML at the moment of the issue. Hard-to-reproduce, state-dependent bugs — expired tokens, feature-flag branches, stale client caches — leave a clear trail. The trade-off is exposure risk, so the feature is off by default and turned on per project.
Why Opt-in
localStorage and cookies typically hold auth tokens, user IDs, and feature flags. The DOM can leak passwords being typed, payment card numbers, or another user's PII. Sweeping that data up indiscriminately turns one issue report into a security incident.
QA Note's rules:
- Off by default — New projects ship with Storage / DOM capture disabled.
- Per-project, not per-org — Activation is at the project level.
- Client-side masking first — Masking runs in the Extension / Widget before data leaves the browser. Passwords and tokens never reach the server.
- Explicit scope — The user is told "this issue includes storage / DOM" right before capture.
How to Enable
Find it in the dashboard.
Project Settings → Capture Options → toggle Storage / DOM on
Path: /settings/projects/[projectId]/capture
The toggle is forward-only. Existing issues are not touched. Toggling off stops capture on the next issue; existing captured data persists until deleted via the API or dashboard (see Disable / Delete).
What Gets Captured
| Item | Shape | Size cap | Masking |
|---|---|---|---|
localStorage | Record<string, string> | 2KB per key · 50KB total | ✅ |
sessionStorage | Record<string, string> | 2KB per key · 50KB total | ✅ |
cookies | Array<{ name, value, domain }> | 30KB total | ✅ |
DOM outerHTML | string (gzip) | 500KB after gzip | ✅ |
Values past the cap get truncated with a ... [truncated] marker. The DOM cap is measured on the gzipped size.
Masking SSoT — 14 + 13 + 14
The masking rules live in a single module — one source of truth.
Real filter definitions:
- apps/extension/src/lib/masking/patterns.ts (extension)
- apps/web/src/lib/capture/masking.ts (server-side reinforcement, planned)
Three filter sets:
| Set | Count | Examples |
|---|---|---|
| URL / query params | 14 | password, token, access_token, refresh_token, secret, api_key, apikey, auth, authorization, session, sessionid, csrf, state, code |
| HTTP headers | 13 | Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token, X-Session-Id, Proxy-Authorization, Www-Authenticate, X-Access-Token, X-Refresh-Token, X-Forwarded-For, X-Real-IP |
| Request / response body fields | 14 | password, password_confirmation, currentPassword, newPassword, token, accessToken, refreshToken, secret, apiKey, creditCard, cardNumber, cvv, ssn, pin |
Each set runs case-insensitive exact match plus substring match (e.g., *token*).
Matched values are replaced with ***; only the original length is preserved (Authorization: Bearer eyJ... → Authorization: ***).
DOM Masking Rules
The DOM outerHTML is sanitized before storage.
| Rule | Action |
|---|---|
<script> tags | Removed entirely (avoid XSS / inlined token leaks) |
<input type="password"> | value attribute removed |
<input type="email">, type="tel" | Preserved by default; project option to remove |
data-password, data-token, data-secret, data-sensitive | Attribute removed |
| File-upload inputs | value removed (no filename leakage) |
Iframe contents are captured only when same-origin. Cross-origin iframes are reduced to a placeholder, no inner DOM.
Querying via MCP
Two tools.
get_storage → localStorage · sessionStorage · cookies (post-mask)
get_dom_snapshot → DOM outerHTML (decompressed)
Sample get_storage response:
{
"localStorage": {
"user_id": "usr_123",
"auth_token": "***",
"feature_flags": "{\"new_billing\":true}"
},
"sessionStorage": {
"draft_issue": "..."
},
"cookies": [
{ "name": "session", "value": "***", "domain": ".qanote.app" }
]
}
Only post-mask data is returned, so it is safe to drop directly into an LLM context.
Storage Format · Size
- Compression: The DOM
outerHTMLis gzipped before upload to R2 (typical 80%+ ratio). - Loading: The issue page receives the compressed payload and decompresses on the client.
- Retention: Same retention window as the issue itself (per plan setting).
- Total size: Storage + DOM combined typically stays under 600KB per issue.
Disable · Delete
- Forward-only off: Toggle off in project settings → next issue stops capturing. Existing captured data is not touched.
- Delete existing data: Run the API or use the dashboard action.
# Bulk-delete storage + DOM captures from every issue in a project
DELETE /api/projects/[projectId]/captured-data?types=storage,dom
# Delete from a single issue
DELETE /api/issues/[issueId]/captured-data?types=storage,dom
In the dashboard the same operation lives under Project Settings → "Bulk delete captured data".