Skip to content
Latest stable: v0.9.0.

Timeline and reporting

getChaosLog() returns the full structured event stream. The reporting utilities turn that stream into a stable, file-shaped artifact you can attach to CI, drop into a PR comment, or open locally as a self-contained HTML timeline.

import { writeFileSync } from 'node:fs';
import {
buildChaosReport,
formatReportHtml,
getChaosLog,
getChaosSeed,
} from '@chaos-maker/playwright';
const events = await getChaosLog(page);
const seed = await getChaosSeed(page);
const report = buildChaosReport(events, { seed, title: 'checkout flow' });
writeFileSync('chaos-report.html', formatReportHtml(report));

The same surface is re-exported from @chaos-maker/cypress, @chaos-maker/puppeteer, and @chaos-maker/webdriverio.

SectionSourceWhat it answers
metaAggregate of the full event arraySeed, title, generated-at, event count, applied/skipped totals, run duration, ready-to-copy replay snippet
ruleHitsDebug stream attributionWhich rules applied or skipped, grouped by ruleId. Requires debug: true (see below)
transportsEvent type prefixNetwork vs WebSocket vs SSE vs UI vs rule-group volume, with applied counts
skipReasonsDebug stream skip stagesWhy rules skipped, grouped by {stage, skippedAt} so you can see the dominant blocker at a glance
failuresOutcome eventsEvery applied failure-class event (network:failure, network:abort, network:cors, network:corruption, or any statusCode >= 500), grouped by rule, type, and status
phasesStreaming events with detail.phaseCounts per lifecycle tag (ai:first-chunk, ai:stream-truncated, …) per transport. Empty for non-streaming runs
connectionsStreaming events with detail.connectionIdOne ordered lifecycle timeline per streamed connection: first-chunk latency, pauses and whether they resolved, truncation and replay flags
streamingReadinessDerived from connectionsDeterministic scorecard for chat smoke tests: how many streams completed without interruption, truncation/replay counts, unresolved pauses, sliced per transport. null when nothing streamed
timelineEvery event in emission orderChronological audit trail with relative +Nms offsets and formatStepTitle() labels, plus phase, chunkIndex, connectionId, and replay mutationIndex when present

meta, transports, failures, phases, connections, streamingReadiness, and timeline populate from outcome events alone and work on any run. ruleHits and skipReasons source from the debug stream, so add debug: true to your config when you want per-rule attribution and structured skip reasons in the report.

Rules resolved from a named matcher keep their matcherName attribution in every debug event, including runs where the config was injected through a framework adapter: the resolver stamps the name onto the rule itself, so it survives serialization into the page.

Every streaming event carries a phase tag (<namespace>:<lifecycle>, e.g. ai:stream-paused), a zero-based chunkIndex, and a stable connectionId. The report groups them into per-connection timelines, so a failed chat scenario reads as a story: stream opened, first chunk at +3000ms, paused at chunk 10, resumed, truncated at chunk 20.

### Connection conn-7f3a (fetch-stream) truncated
- URL: /api/chat
- Events: 4, first chunk at +3000ms, pauses 1
- +3000ms first chunk ai:first-chunk chunk 0
- +3400ms chunk delayed ai:stream-paused chunk 10
- +8400ms stream resumed ai:stream-resumed chunk 10
- +9100ms stream truncated ai:stream-truncated chunk 20

The streamingReadiness block answers the smoke-test question directly: out of N streamed connections, how many completed clean. A pause with no later resume on the same connection counts as unresolved, which is how a stalled-forever stream shows up without any timeout heuristics.

Replayed streams carry phase: 'ai:stream-replayed' on their first event. When a replay mutation fires during the run, the resulting pause, duplicate, or truncation event also carries mutationIndex, the zero-based position in your mutations array, and renders as (mutation N) in Markdown and a chip in HTML, so a broken rendering traces back to the exact mutation entry that caused it. Mutation attribution ships for fetch-stream replay; SSE and WebSocket replay events carry phases and chunk indices but not yet mutation indices.

Human-interaction triggers emit into the same stream under the user: namespace (user:cancel, user:tab-hidden, user:retry, …), so a cancelled generation renders next to the transport events it interrupted. Cancel events additionally carry detail.targetTransport and, for rule-matched fetch streams, the connectionId of the connection they aborted, which joins them into that connection’s timeline.

The Markdown and HTML formatters render the streaming sections only when the run actually streamed, so reports for classic network/UI chaos are unchanged.

formatReportJson, formatReportMarkdown, and formatReportHtml all consume the same ChaosReport and emit strings. The core package never writes to disk; pass the string to fs.writeFileSync or testInfo.attach in your test.

import {
buildChaosReport,
formatReportJson,
formatReportMarkdown,
formatReportHtml,
} from '@chaos-maker/playwright';
const report = buildChaosReport(events, { seed });
const json = formatReportJson(report); // pretty by default; { pretty: false } for one-liner
const md = formatReportMarkdown(report); // GitHub-flavored tables, drop into a PR comment
const html = formatReportHtml(report); // self-contained document, inline CSS, no <script>, no CDNs

The HTML output ships every section as a native <details open> block so reviewers can collapse what they do not need. There is no inline JavaScript and no external URL: opening the file from file:// works the same as serving it.

buildChaosReport(events, opts) is a pure function. Given the same events, seed, title, and now it always produces the same report, and every formatter emits the same string for the same report. Two guards keep CI artifacts diff-friendly:

  • The timeline renders relative offsets (+0ms, +125ms), not absolute wall-clock times. Two runs that reproduce the same chaos sequence produce the same timeline rendering, even when the absolute timestamp values differ.
  • All aggregates have an explicit total ordering (counts desc, then names asc) so re-running a flaky test cannot reorder rows.

In tests that snapshot the formatted output, pin now to a fixed value via buildChaosReport(events, { now: 1_700_000_000_000 }).

filterEventsByTransport(events, kind) returns the subset of events whose type prefix maps to one bucket. The bucket taxonomy mirrors what transports[] reports.

import { filterEventsByTransport } from '@chaos-maker/playwright';
const wsEvents = filterEventsByTransport(events, 'websocket');

Valid kinds: 'network', 'websocket', 'sse', 'fetch-stream', 'ui', 'rule-group'. The single 'network' bucket spans fetch and XHR because today’s ChaosEvent.detail does not distinguish them; 'fetch-stream' is its own bucket because it reports chunk-level lifecycle rather than request outcomes.

The Playwright adapter has the cleanest seam: write the report inside the same afterEach or test body that calls getChaosLog, then attach it with testInfo.attach so the HTML reporter renders a download link on failed runs.

test('checkout survives slow API', async ({ page }, testInfo) => {
await injectChaos(page, { debug: true, network: { latencies: [/* … */] } });
// … run scenario …
const events = await getChaosLog(page);
const seed = await getChaosSeed(page);
const report = buildChaosReport(events, { seed, title: testInfo.title });
await testInfo.attach('chaos-report.html', {
body: formatReportHtml(report),
contentType: 'text/html',
});
});

For Cypress, Puppeteer, and WebdriverIO, write the string to disk from the test process and upload it as a CI artifact using whatever your pipeline already does for screenshots.