pw-sanitizer
v0.3.0
Published
Post-process Playwright HTML reports and trace files to redact secrets and remove noisy steps
Maintainers
Readme
playwright-sanitizer
Post-process Playwright HTML reports and trace files to redact secrets and remove noisy steps before sharing or archiving.
Why
Playwright's HTML reports and .zip trace files can contain sensitive data — auth headers, API keys, tokens, passwords — embedded in network request logs, action parameters, and step metadata. playwright-sanitizer scans those files after your test run and replaces or strips the sensitive content so they are safe to store in CI artifacts, share with teammates, or upload to a dashboard.
It also lets you delete repetitive or internal steps (e.g. health-check pings, internal logging actions) that create noise in the trace viewer.
Features
- Redact secrets in HTML reports and trace
.zipfiles by key name, value pattern, or both - Remove entire steps from traces with configurable timestamp repair
- Three output modes:
copy(safe default),in-place,side-by-side - Zero built-in patterns — you declare exactly what gets touched, nothing else runs
- Dry-run mode to preview changes without writing files
- Integrates as a Playwright reporter (
pw-sanitizer/reporter) so it runs automatically after every test run — after the HTML report is written - Programmatic API for custom pipelines
- Summary table printed to the console (and optionally written as JSON)
Supported Playwright formats
As of v0.2.0, the processors work against the artifact formats real Playwright versions actually produce (verified against @playwright/test 1.40–1.61):
- Trace
.zipfiles: every*.traceentry (test.trace,0-trace.trace, ...) is processed as NDJSON — one JSON event per line, with steps stored as paired"before"/"after"events linked bycallId/parentId.*.networkentries are handled the same way, andresources/files are redacted. Removal rules match against the"before"event titles (title, orapiNameon older versions); descendants are found by walking the fullparentIdtree, including nestedtest.stepandpw:apichildren. Archives with a legacytrace.jsonare still supported as a fallback. - HTML reports: the report data embedded as a base64-encoded zip (
window.playwrightReportBase64 = "..."on older versions,<template id="playwrightReportBase64">on newer ones) is decoded, itsreport.jsonand per-test-file JSON shards are sanitized (step trees pruned perorphanStrategy), then re-zipped and substituted back into the HTML. The pre-base64window.__pw_report_data__format is still supported as a fallback.
Integration tests run against genuine artifacts generated by real Playwright runs (see tests/fixtures/real/README.md).
Requirements
- Node.js >= 18
Installation
npm install --save-dev pw-sanitizerScreenshot redaction inside trace files requires the optional sharp dependency:
npm install --save-dev sharpQuick start
1. Create a config file
// playwright-sanitizer.config.ts
import type { SanitizerConfig } from 'pw-sanitizer';
const config: SanitizerConfig = {
redact: {
patterns: [
{
id: 'auth-header',
description: 'Authorization header value',
key: 'authorization',
severity: 'critical',
},
{
id: 'api-key',
key: /x-api-key/i,
severity: 'high',
},
],
},
remove: {
rules: [
{
label: 'health-check pings',
url: /\/health$/,
},
],
},
};
export default config;2. Run the CLI
npx pw-sanitizerSanitized files are written to ./sanitized-report by default. Originals are untouched.
Configuration
Config is auto-discovered in this priority order:
| Priority | Source |
|----------|--------|
| 1 | --config <path> CLI flag |
| 2 | playwright-sanitizer.config.ts |
| 3 | playwright-sanitizer.config.js |
| 4 | playwright-sanitizer.config.json |
| 5 | sanitizer key inside playwright.config.ts |
Full config reference
[!NOTE] TypeScript configuration files (
.ts) are supported natively by default usingjiti. No separate runner or transpilation step is required.
import type { SanitizerConfig } from 'pw-sanitizer';
const config: SanitizerConfig = {
redact: {
// Inline patterns (merged with patternFiles)
patterns: [
{
id: 'my-token', // REQUIRED — unique identifier shown in summary
description: 'API token', // optional, shown in summary output
key: 'x-api-token', // match field/header name (string = case-insensitive exact match)
valuePattern: /^ey/, // match field value (RegExp) — when set alongside key, BOTH must match
severity: 'high', // 'low' | 'medium' | 'high' | 'critical' (informational only)
},
],
// External pattern files (.ts, .js, or .json) — merged with inline patterns
patternFiles: ['./secrets/patterns.ts'],
// Replacement string. Default: '[REDACTED]'
placeholder: '[REDACTED]',
// Partial redaction: keep first N and last N characters, mask the middle with '***'
// Example: { prefix: 4, suffix: 4 } on 'Bearer eyJhbGci...' => 'Bear***..ci'
// Takes priority over placeholder when set.
partialRedaction: { prefix: 4, suffix: 4 },
},
remove: {
// Inline rules (merged with ruleFiles)
rules: [
{
label: 'noisy health checks', // REQUIRED — shown in summary and dry-run log
stepName: /health/i, // match step name shown in the HTML report
selector: '[data-testid="spinner"]', // match CSS selector or XPath locator
url: /\/internal\//, // match network request URL
actionType: 'waitForTimeout', // match Playwright internal action type
// Safety guard: only remove if this step appears at least N times
// consecutively within a test. Logs a warning and skips removal if not met.
minConsecutiveOccurrences: 3,
},
],
// External rule files (.ts, .js, or .json) — merged with inline rules
ruleFiles: ['./rules/remove-rules.ts'],
// How to handle time after a step is deleted.
// 'absorb-into-prev' (default): preceding step absorbs the deleted duration
// 'absorb-into-next': following step's start time shifts back
// 'gap': no adjustment; a gap appears in the timeline
timestampStrategy: 'absorb-into-prev',
// What to do with child steps when a parent is removed.
// 'remove-children' (default): children are also removed
// 'keep-shell': parent is kept as an empty container
orphanStrategy: 'remove-children',
// Preview mode — log what would be removed but don't write any files
dryRun: false,
},
output: {
reportDir: './playwright-report', // where to find HTML reports. Default: './playwright-report'
// Where to find trace zips — string or array. Default: './test-results'
// RECOMMENDED: include the report's data/ folder too. The HTML report's
// built-in trace viewer opens the trace COPIES under playwright-report/data,
// so sanitizing test-results alone leaves those copies dirty.
// Overlapping matches are deduplicated by absolute path.
traceDir: ['./test-results', './playwright-report/data'],
// Output mode:
// 'copy' (default): write sanitized files to dir, originals untouched
// 'in-place': overwrite original files (ensure they are version-controlled!)
// 'side-by-side': write '<name>.sanitized.<ext>' next to each original
mode: 'copy',
dir: './sanitized-report', // destination for 'copy' mode. Default: './sanitized-report'
processReports: true, // set to false to skip HTML report processing
processTraces: true, // set to false to skip trace file processing
redactScreenshots: false, // redact screenshots in traces (requires 'sharp')
},
reporting: {
summary: true, // print summary table after processing. Default: true
summaryFile: './sanitization-summary.json', // optional — write summary as JSON to this path
logLevel: 'normal', // 'silent' | 'normal' | 'verbose'
},
};
export default config;External pattern and rule files
For large teams, keep patterns and rules in dedicated files so they can be versioned and shared across projects:
// secrets/patterns.ts
import type { RedactPattern } from 'pw-sanitizer';
const patterns: RedactPattern[] = [
{ id: 'auth-header', key: 'authorization', severity: 'critical' },
{ id: 'cookie', key: 'cookie', severity: 'high' },
{ id: 'set-cookie', key: 'set-cookie', severity: 'high' },
];
export default patterns;// playwright-sanitizer.config.ts
export default {
redact: { patternFiles: ['./secrets/patterns.ts'] },
};JSON files are also supported. Note that RegExp is not available in JSON — strings are matched as case-insensitive exact key names:
[
{ "id": "auth-header", "key": "authorization", "severity": "critical" },
{ "id": "api-key", "key": "x-api-key", "severity": "high" }
]CLI reference
Usage: pw-sanitizer [options]
Options:
-c, --config <path> Path to config file
-r, --report <path> HTML report directory (default: "./playwright-report")
-t, --traces <path...> Trace directory — repeatable (default: "./test-results")
-o, --output <path> Output directory (copy mode)
--in-place Overwrite original files
--patterns <path...> One or more pattern files
--placeholder <string> Redaction placeholder (default: "[REDACTED]")
--dry-run Log changes without writing files
--no-traces Skip trace file processing
--no-reports Skip HTML report processing
--summary-output <path> Write JSON summary to file
--log-level <level> silent | normal | verbose (default: "normal")
-V, --version Display version number
-h, --help Display helpPrecedence is strictly CLI flag > config file > built-in default. A flag only overrides its config-file counterpart when it is actually passed on the command line.
Examples
# Auto-discover config and run with defaults
npx pw-sanitizer
# Use a specific config file
npx pw-sanitizer --config ./ci/sanitizer.config.ts
# Overwrite originals (ensure files are version-controlled)
npx pw-sanitizer --in-place
# Preview without writing any files
npx pw-sanitizer --dry-run --log-level verbose
# Point to non-default directories
npx pw-sanitizer --report ./reports --traces ./artifacts/traces --output ./sanitized
# Cover both trace locations (test-results + the report's data/ copies)
npx pw-sanitizer --traces ./test-results ./playwright-report/data --in-place
# Write a machine-readable summary for CI artifact ingestion
npx pw-sanitizer --summary-output ./sanitization-summary.jsonPlaywright reporter integration (recommended)
Register the sanitizer as a Playwright reporter and it runs automatically after every test run:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html'],
['pw-sanitizer/reporter'], // list last by convention
],
// ...rest of your config
});This is the right hook for sanitizing HTML reports. Playwright's end-of-run order is:
- tests finish
globalTeardownrunsreporter.onEnd()for each reporter — this is where the HTML report and theplaywright-report/data/*.ziptrace copies get writtenreporter.onExit()for each reporter — wherepw-sanitizer/reporterruns
Because all onEnd() calls complete before any onExit(), correctness does not depend on reporter order — listing the sanitizer last is just a readability convention. The reporter never prints to stdio, and sanitizer failures are caught and logged so they can never mask your test results.
Options: pass configPath to skip auto-discovery — ['pw-sanitizer/reporter', { configPath: './ci/sanitizer.config.ts' }].
[!TIP] Pair this with
traceDir: ['./test-results', './playwright-report/data']so the trace copies that the report's built-in trace viewer opens get sanitized too.
globalTeardown integration (trace-only)
[!WARNING] Playwright runs
globalTeardownbefore reporters write their output. When this hook fires, the current run's HTML report and itsdata/trace copies do not exist yet — at best you would sanitize the previous run's report. Only use this integration when you exclusively sanitizetest-resultstraces; otherwise usepw-sanitizer/reporterabove.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalTeardown: require.resolve('pw-sanitizer/teardown'),
// ...rest of your config
});Config is auto-discovered as described above. Teardown failures are caught and logged — they will never mask your actual test results.
Embedding config in playwright.config.ts
If you prefer a single config file, add a sanitizer key directly to your Playwright config:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { SanitizerConfig } from 'pw-sanitizer';
export default defineConfig({
reporter: [['html'], ['pw-sanitizer/reporter']],
sanitizer: {
redact: {
patterns: [
{ id: 'auth', key: 'authorization', severity: 'critical' },
],
},
output: {
mode: 'in-place',
traceDir: ['./test-results', './playwright-report/data'],
},
} satisfies SanitizerConfig,
});Programmatic API
import { sanitize, redactReport, redactTrace } from 'pw-sanitizer';
// Process all reports and traces using auto-discovered config
const results = await sanitize();
// Pass a config object directly (skips file discovery)
const results = await sanitize({
redact: { patterns: [{ id: 'token', key: 'x-token' }] },
output: { mode: 'copy', dir: './out' },
});
// Process a single HTML report
const result = await redactReport('./playwright-report/index.html', config);
// Process a single trace zip
const result = await redactTrace('./test-results/my-test/trace.zip', config);Each call returns a ProcessResult (or an array of them for sanitize()):
interface ProcessResult {
file: string;
redactionsApplied: number;
stepsRemoved: number;
timestampRepairs: number;
redactionMatches: Array<{ keyPath: string; patternId: string }>;
removalMatches: Array<{ index: number; ruleLabel: string; event: TraceEvent }>;
}Output modes
| Mode | Behaviour |
|------|-----------|
| copy (default) | Writes sanitized files to output.dir (./sanitized-report). Originals are never touched. |
| in-place | Overwrites original files. A warning is printed. Use only with version control. |
| side-by-side | Creates <name>.sanitized.<ext> next to each original file. |
How redaction works
Redaction walks every JSON structure inside the HTML report's embedded data and the trace file's event entries. For each field encountered, the declared patterns are checked:
- If
keyis a string — matched case-insensitively as an exact field name. - If
keyis a RegExp — tested against the field name. - If
valuePatternis provided — bothkeyandvaluePatternmust match (AND logic). - The matched value is replaced with
placeholder, or a partial redaction ifpartialRedactionis configured.
No built-in patterns are ever applied. Only what you declare runs.
How step removal works
Step removal operates on the trace event tree inside each .zip file:
- Each event is tested against your declared rules (
stepName,selector,url,actionType). - Matching events are collected and removed from the event list.
- Timestamps of surrounding events are adjusted according to
timestampStrategyto keep the timeline coherent. - Child steps of removed parents are handled according to
orphanStrategy.
The minConsecutiveOccurrences safety guard prevents accidentally removing a step that only appears occasionally — if the actual consecutive count is below the threshold, the step is not removed and a warning is logged instead.
CI integration example
# .github/workflows/test.yml
- name: Run Playwright tests
run: npx playwright test
- name: Sanitize Playwright artifacts
run: npx pw-sanitizer --summary-output sanitization-summary.json
- name: Upload sanitized report
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: sanitized-report/License
MIT
