Skip to content

JSONL stream

The JSONL reporter writes one JSON object per line, a complete event stream of the run. It is the richest of the four reporters: every step and every UI action carries its full request, response, and timing data.

Terminal window
tales test ./e2e/pass --report-jsonl ./reports/events.jsonl

The file is written after the suite completes (not incrementally). The parent directory is created if missing.

Each line carries a type field. There are three:

typeEmitted when
scenarioOnce per scenario, summarising its final status, duration, and (when failed/skipped) error / skip reason.
stepOnce per step. Includes a phase field set to "step" for main-flow steps and "teardown" for teardown steps. Retried steps surface their attempt count via attempts.
actionOnce per UI action for mobile steps that recorded actions (any --capture-screenshots mode other than none). Emitted immediately after the parent step event.

There is no separate suite_start / suite_end envelope. Scenario aggregation is up to the consumer (jq -s 'group_by(.scenario)' or similar).

{"type":"scenario","scenario":"Create blog post","file":"e2e/pass/blog.tales","status":"pass","duration_ms":842,"seed":1234}
{"type":"step","phase":"step","scenario":"Create blog post","step":"create_user","provider":"http","status":"pass","status_code":201,"duration_ms":312,"seed":1234,"file":"e2e/pass/blog.tales","request":{"method":"POST","url":"http://localhost:1337/users"},"response":{"status_code":201,"json":{"id":"u_abc"}}}
{"type":"step","phase":"step","scenario":"Create blog post","step":"create_post","provider":"http","status":"pass","status_code":201,"duration_ms":418,"seed":1234,"file":"e2e/pass/blog.tales"}
{"type":"step","phase":"teardown","scenario":"Create blog post","step":"delete_user","provider":"http","status":"pass","status_code":204,"duration_ms":112,"seed":1234,"file":"e2e/pass/blog.tales"}

The order is scenario event first, then its step events (main flow, then teardown) interleaved with any action events.

FieldTypeNotes
typestringAlways "scenario".
scenariostringScenario name.
filestringPath to the .tales file that defines the scenario.
statusstringpass, fail, skipped.
duration_msnumberEnd-to-end wall-clock duration of the scenario.
seednumberThe --seed value used for this run.
errorobjectWhen failed: kind, path, want, got, message.
skip_reasonstringPresent only when status="skipped". Omitted otherwise.
FieldTypeNotes
typestringAlways "step".
phasestring"step" for main-flow steps; "teardown" for teardown steps.
scenariostringScenario name.
stepstringStep name.
providerstringhttp, sql, mobile, keyword.
statusstringpass, fail, skipped.
duration_msnumberStep duration including retries.
seednumberThe --seed value used for this run.
filestringPath to the .tales file that defines the step.
status_codenumberHTTP status code, when applicable. Omitted for non-HTTP steps.
attemptsnumberTotal attempts run, present only when greater than 1 (the step retried). A single-attempt step omits the field entirely.
skip_reasonstringPresent only when status="skipped". Omitted otherwise.
requestobjectProvider-specific request shape. Omitted when empty.
responseobjectProvider-specific response shape. Omitted when empty.
actionsarrayMobile: per-action records, mirrored at the request level.
artifactsobjectMobile: paths to screenshot / hierarchy files. Omitted when empty.
errorobjectWhen failed: kind, path, want, got, message.

One per UI action, emitted after the step event when step.actions is populated. Empty action arrays produce byte-identical JSONL to the pre-action-stream format, so legacy consumers keep working.

{
"type": "action",
"scenario": "iOS register",
"step": "fill_form",
"index": 0,
"kind": "input_text",
"label": "register.email",
"value": "[email protected]",
"status": "pass",
"duration_ms": 42,
"screenshot": "build/artifacts/mobile/.../screenshot.png",
"hierarchy": "build/artifacts/mobile/.../hierarchy.json"
}

Secure values are masked to "***" at one boundary inside the mobile provider, see Visual HTML report.

The SQL provider scrubs DSNs from every event before serialisation. Only the connection name, driver alias, SQL text, and arg count appear, never the connection string. See SQL provider for the full rules.

auth.basic.password is masked in HTTP request shapes. Other secrets injected via headers are not masked, capture them from upstream responses if you need masking.

Terminal window
# Count failures
jq -c 'select(.type=="step" and .status=="fail")' events.jsonl | wc -l
# All failure messages with their step path
jq -c 'select(.type=="step" and .status=="fail") | {scenario, step, msg:.failure.message}' events.jsonl
# Total wall-clock time across scenarios (parallelism aware)
jq -s 'map(select(.type=="scenario") | .duration_ms) | add' events.jsonl
# All retried steps with their attempt count
jq -c 'select(.type=="step" and .attempts) | {scenario, step, attempts}' events.jsonl

JSONL plays nicely with Loki, Elasticsearch, BigQuery, Snowflake, Datadog logs. Ingest the file once at suite end and query at will.