Observability
Every chaos decision emits a ChaosEvent.
type ChaosEvent = { type: string; timestamp: number; applied: boolean; detail: Record<string, unknown>;};Use the adapter log helpers to assert that chaos happened and to diagnose skipped decisions.
const log = await getChaosLog(page);expect(log.some((event) => event.type === 'network:failure' && event.applied)).toBe(true);Event taxonomy
Section titled “Event taxonomy”| Event type | Fires when | Detail keys | Example assertion |
|---|---|---|---|
network:failure | A matching failure rule returns or skips a configured HTTP status. | url, method, statusCode, optional operationName, reason | log.some((e) => e.type === 'network:failure' && e.detail.statusCode === 503) |
network:latency | A matching latency rule delays or skips a request. | url, method, delayMs, optional operationName, reason | log.some((e) => e.type === 'network:latency' && e.detail.delayMs === 800) |
network:abort | A matching abort rule aborts, skips, or loses the race to a completed request. | url, method, timeoutMs, optional operationName, reason | log.some((e) => e.type === 'network:abort' && e.applied) |
network:corruption | A matching corruption rule changes, skips, or cannot change a response body. | url, method, strategy, optional operationName, reason | log.some((e) => e.type === 'network:corruption' && e.detail.strategy === 'truncate') |
network:cors | A matching CORS rule simulates a browser network failure or skips by probability. | url, method, optional operationName, reason | log.some((e) => e.type === 'network:cors' && e.applied) |
ui:assault | A DOM assault rule sees a matching element and applies or skips the action. | selector, action, optional groupName | log.some((e) => e.type === 'ui:assault' && e.detail.action === 'disable') |
websocket:drop | A WebSocket drop rule drops an inbound or outbound frame. | url, direction, payloadType, optional reason | log.some((e) => e.type === 'websocket:drop' && e.detail.direction === 'inbound') |
websocket:delay | A WebSocket delay rule delays an inbound or outbound frame. | url, direction, payloadType, delayMs | log.some((e) => e.type === 'websocket:delay' && e.detail.delayMs === 500) |
websocket:corrupt | A WebSocket corruption rule changes a compatible frame or records why it could not. | url, direction, payloadType, strategy, optional reason | log.some((e) => e.type === 'websocket:corrupt' && e.detail.strategy === 'truncate') |
websocket:close | A WebSocket close rule closes the socket. | url, closeCode, closeReason | log.some((e) => e.type === 'websocket:close' && e.detail.closeCode === 1011) |
sse:drop | An SSE drop rule drops a matching event. | url, eventType, optional reason | log.some((e) => e.type === 'sse:drop' && e.detail.eventType === 'token') |
sse:delay | An SSE delay rule delays a matching event. | url, eventType, delayMs | log.some((e) => e.type === 'sse:delay' && e.detail.delayMs === 800) |
sse:corrupt | An SSE corruption rule changes a matching event payload. | url, eventType, strategy | log.some((e) => e.type === 'sse:corrupt' && e.detail.strategy === 'malformed-json') |
sse:close | An SSE close rule closes the stream. | url, reason | log.some((e) => e.type === 'sse:close') |
rule-group:enabled | enableGroup() or an adapter group helper enables a group. | groupName | log.some((e) => e.type === 'rule-group:enabled' && e.detail.groupName === 'payments') |
rule-group:disabled | disableGroup() or an adapter group helper disables a group. | groupName | log.some((e) => e.type === 'rule-group:disabled' && e.detail.groupName === 'payments') |
rule-group:gated | A rule is skipped because its group is disabled. | groupName, plus the rule context such as url, method, selector, or action | log.some((e) => e.type === 'rule-group:gated' && e.detail.groupName === 'payments') |
debug | debug: true records each rule-decision stage and lifecycle event. | stage, optional phase, ruleType, ruleId, ruleName, url, method, selector, action, groupName, enabled | log.some((e) => e.type === 'debug' && e.detail.stage === 'rule-skip-probability') |
Always log getChaosSeed() when a test fails. The seed plus the chaos log gives you the replay input and the observed decision sequence.
import { formatSeedReproduction, getChaosSeed } from '@chaos-maker/playwright';
console.error(formatSeedReproduction(await getChaosSeed(page)));For a complete loop, see Reproduce a flaky failure.
Debug Mode
Section titled “Debug Mode”When chaos does not fire, set debug: true on your config to make Chaos Maker log every step of its rule decision pipeline.
await injectChaos(page, { debug: true, network: { failures: [{ urlPattern: '/api/payments', statusCode: 503, probability: 1 }], },});Two sinks fire on rule decisions and lifecycle events (e.g. engine:start, sw:config-applied):
- Console mirror: a single line per stage to
console.debug. Open browser DevTools to see them; CI loggers hideconsole.debugby default. - Structured event: a
type: 'debug'event through the existing emitter. Lands ingetChaosLog(), the Playwrightchaos-log.jsonattachment, and the Service Worker broadcast bridge.
Sample console output:
[Chaos] rule-evaluating: rule=failure#0 GET /api/payments -> 503[Chaos] rule-matched: rule=failure#0 GET /api/payments -> 503[Chaos] rule-applied: rule=failure#0 GET /api/payments -> 503[Chaos] lifecycle: engine:startService Worker chaos uses a distinct prefix so you can split the streams when both sinks log to the same console:
[Chaos SW] lifecycle: sw:config-applied[Chaos SW] rule-applied: rule=failure#0 GET /api/payments -> 503The structured event keeps the stage on detail.stage. Subscribe with one listener and switch on the stage:
instance.on('debug', (event) => { if (event.detail.stage === 'rule-applied') { myReporter.record(event); }});The full stage taxonomy is documented on the Debug API reference.
Why did chaos not fire?
Section titled “Why did chaos not fire?”Start from the applied: false events:
const misses = log.filter((event) => event.applied === false);type: 'rule-group:gated' means the rule matched, but its group was disabled. Check detail.groupName, then call the matching page or Service Worker group helper before the triggering action.
detail.reason === 'graphql-body-unparseable' means a rule with graphqlOperation needed the request body, but the body could not be parsed. Match by URL only, remove the GraphQL constraint, or send JSON that contains operationName or a parseable query.
detail.reason === 'incompatible-payload-type' means a corruption strategy expected text, but the WebSocket frame was binary. Use a text payload or choose a rule that supports binary frames.
type: 'debug' gives the exact stage:
| Stage | Meaning | Typical fix |
|---|---|---|
rule-skip-match | URL, method, selector, event type, direction, or GraphQL operation did not match. | Compare the rule matcher with the emitted detail fields. |
rule-skip-counting | onNth, everyNth, or afterN has not reached an active count. | Trigger the action enough times or adjust the counter. |
rule-skip-group | The rule’s group is disabled. | Enable the group before the action. |
rule-skip-probability | The seeded probability roll missed. | Replay with the same seed to confirm, or raise probability. |
For a step-by-step workflow, see Diagnose no chaos.
Framework-agnostic by design
Section titled “Framework-agnostic by design”Debug Mode is independent of your test runner’s debug flags. It does not read PWDEBUG, --debug, DEBUG=cypress:*, DEBUG=puppeteer:*, DEBUG=wdio:*, localStorage.debug, or any other framework-owned signal. The only switch is debug on ChaosConfig.
CI guidance
Section titled “CI guidance”console.debug is filtered out by default in Playwright, Cypress, Puppeteer, and Vitest reporters. Even so, prefer to omit debug in CI configs unless you are deliberately collecting decision logs. Debug events also ride through getChaosLog() and inflate the log buffer.
Matcher attribution
Section titled “Matcher attribution”Every type: 'debug' event emitted for a rule that came from a named matcher (see Advanced matchers) carries detail.matcherName: '<name>'. The rule-matched stage additionally carries detail.matchedBy: string[] listing which non-URL matchers fired - one of 'hostname', 'queryParams', 'requestHeaders', 'resourceTypes', 'graphqlOperation'. The rule-skip-match stage carries detail.skippedAt: string naming the first matcher field that failed. These three fields make “why did this rule fire?” and “why did this rule skip?” trivially readable from the structured log.
Playwright traces
Section titled “Playwright traces”The Playwright adapter can add chaos events to the trace viewer and attach chaos-log.json at test end. type: 'debug' events land in the JSON attachment but never render as inline test.step entries, so the action timeline stays focused on real chaos decisions.
Timeline and reporting artifacts
Section titled “Timeline and reporting artifacts”For a structured, file-shaped view of a run, hand the same event log to buildChaosReport() and pick a serializer. See Timeline and reporting for the full output shape and CI integration patterns.