Skip to content
Latest stable: v0.8.0.

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 typeFires whenDetail keysExample assertion
network:failureA matching failure rule returns or skips a configured HTTP status.url, method, statusCode, optional operationName, reasonlog.some((e) => e.type === 'network:failure' && e.detail.statusCode === 503)
network:latencyA matching latency rule delays or skips a request.url, method, delayMs, optional operationName, reasonlog.some((e) => e.type === 'network:latency' && e.detail.delayMs === 800)
network:abortA matching abort rule aborts, skips, or loses the race to a completed request.url, method, timeoutMs, optional operationName, reasonlog.some((e) => e.type === 'network:abort' && e.applied)
network:corruptionA matching corruption rule changes, skips, or cannot change a response body.url, method, strategy, optional operationName, reasonlog.some((e) => e.type === 'network:corruption' && e.detail.strategy === 'truncate')
network:corsA matching CORS rule simulates a browser network failure or skips by probability.url, method, optional operationName, reasonlog.some((e) => e.type === 'network:cors' && e.applied)
ui:assaultA DOM assault rule sees a matching element and applies or skips the action.selector, action, optional groupNamelog.some((e) => e.type === 'ui:assault' && e.detail.action === 'disable')
websocket:dropA WebSocket drop rule drops an inbound or outbound frame.url, direction, payloadType, optional reasonlog.some((e) => e.type === 'websocket:drop' && e.detail.direction === 'inbound')
websocket:delayA WebSocket delay rule delays an inbound or outbound frame.url, direction, payloadType, delayMslog.some((e) => e.type === 'websocket:delay' && e.detail.delayMs === 500)
websocket:corruptA WebSocket corruption rule changes a compatible frame or records why it could not.url, direction, payloadType, strategy, optional reasonlog.some((e) => e.type === 'websocket:corrupt' && e.detail.strategy === 'truncate')
websocket:closeA WebSocket close rule closes the socket.url, closeCode, closeReasonlog.some((e) => e.type === 'websocket:close' && e.detail.closeCode === 1011)
sse:dropAn SSE drop rule drops a matching event.url, eventType, optional reasonlog.some((e) => e.type === 'sse:drop' && e.detail.eventType === 'token')
sse:delayAn SSE delay rule delays a matching event.url, eventType, delayMslog.some((e) => e.type === 'sse:delay' && e.detail.delayMs === 800)
sse:corruptAn SSE corruption rule changes a matching event payload.url, eventType, strategylog.some((e) => e.type === 'sse:corrupt' && e.detail.strategy === 'malformed-json')
sse:closeAn SSE close rule closes the stream.url, reasonlog.some((e) => e.type === 'sse:close')
rule-group:enabledenableGroup() or an adapter group helper enables a group.groupNamelog.some((e) => e.type === 'rule-group:enabled' && e.detail.groupName === 'payments')
rule-group:disableddisableGroup() or an adapter group helper disables a group.groupNamelog.some((e) => e.type === 'rule-group:disabled' && e.detail.groupName === 'payments')
rule-group:gatedA rule is skipped because its group is disabled.groupName, plus the rule context such as url, method, selector, or actionlog.some((e) => e.type === 'rule-group:gated' && e.detail.groupName === 'payments')
debugdebug: true records each rule-decision stage and lifecycle event.stage, optional phase, ruleType, ruleId, ruleName, url, method, selector, action, groupName, enabledlog.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.

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):

  1. Console mirror: a single line per stage to console.debug. Open browser DevTools to see them; CI loggers hide console.debug by default.
  2. Structured event: a type: 'debug' event through the existing emitter. Lands in getChaosLog(), the Playwright chaos-log.json attachment, 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:start

Service 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 -> 503

The 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.

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:

StageMeaningTypical fix
rule-skip-matchURL, method, selector, event type, direction, or GraphQL operation did not match.Compare the rule matcher with the emitted detail fields.
rule-skip-countingonNth, everyNth, or afterN has not reached an active count.Trigger the action enough times or adjust the counter.
rule-skip-groupThe rule’s group is disabled.Enable the group before the action.
rule-skip-probabilityThe seeded probability roll missed.Replay with the same seed to confirm, or raise probability.

For a step-by-step workflow, see Diagnose no chaos.

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.

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.

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.

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.

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.