Skip to content

ReportLens v0.1.7

Choose a tag to compare

@deekshith-poojary98 deekshith-poojary98 released this 07 May 20:22
· 6 commits to main since this release
3b6ffdd

1. Version Summary

Candidate version: 0.1.7
Branch: optimize-compact-mode
Base version: 0.1.6

This release closes the scalability gap in external-data mode. The primary goal was to make large-suite reports (10k+ tests) practical to store, transfer, and serve without special infrastructure. The work proceeded in two sequential phases:

Phase 1 cleaned up the serialisation pipeline — removing global-state log-level configuration, introducing explicit min_log_level propagation, adding dataclass field defaults that allow compact JSON emission (falsy values omitted), and fixing a duplicated endTime field in the keyword serializer.

Phase 2 added gzip compression of the external-data JSON files. Two modes were introduced: --compress-data (writes both .json and .json.gz, full browser compatibility, recommended for CI artefacts) and --compress-data-only (writes only .json.gz, ~97% smaller output, requires a modern browser). The frontend was extended with a native DecompressionStream loading path, an explicit capability guard that blocks initialisation on incompatible browsers with a visible error banner, and two new report-config flags (compressed, compressedOnly) to distinguish the modes. A --loglevel CLI flag was also added to give users explicit control over which log messages are included at generation time.

The architectural theme of this release is explicit over implicit: every previously implicit behaviour (log level defaults, browser capability assumptions, configuration flags) is now either documented, configurable, or enforced with a clear error.


2. What's New

--compress-data (dual-write gzip compression)

What it does: When used with --external-data, writes a .json.gz sibling alongside every .json file in reportlens-data/. The HTML shell detects browser capability at startup via typeof DecompressionStream !== "undefined" and prefers the compressed variant where available, with automatic silent fallback to the plain .json file.

Why it matters: External-data mode with 10k tests previously wrote ~650 MB of JSON. With compression that drops to ~20 MB — a 33× reduction. This makes the output practical for CI artefact storage (GitHub Actions, S3), team distribution, and repository commits.

User-facing behaviour: Transparent to end users. The report loads identically regardless of which format is served. No server-side gzip configuration is required; decompression is entirely client-side.

New report-config flag: "compressed": true is injected into the embedded <script id="report-config"> element so the frontend JS can read it without parsing the file list.


--compress-data-only (gz-only, minimum output size)

What it does: Like --compress-data but suppresses writing plain .json files entirely. Only .json.gz files exist on disk.

Why it matters: --compress-data in dual-write mode produces ~703 MB (both formats) — larger than having no compression at all. --compress-data-only eliminates the plain files and delivers the ~20 MB target directly.

User-facing behaviour: The report will not load in browsers that lack DecompressionStream (Chrome < 80, Firefox < 113, Safari < 16.4). This is enforced by an early capability guard (see below) rather than degrading silently.

New report-config flags: "compressed": true and "compressedOnly": true. The frontend reads compressedOnly to decide whether to run the capability guard.


Frontend capability guard for --compress-data-only

What it does: Immediately after config parsing, the JS checks:

if (compressedOnly && !supportsDecompressionStream) {  }

If the condition is true the script appends a full-screen styled error banner to document.body explaining the incompatibility (browser version floor, MDN link, regeneration hint) and then throws, stopping all further initialisation.

Why it matters: Without this guard, an incompatible browser would silently queue every fetch as a failure — 10,000+ network errors, a blank UI, and no indication of what went wrong. The guard converts a silent data failure into a visible, actionable error.


--loglevel CLI flag

What it does: Controls the minimum log level serialised into the report. Accepts TRACE, DEBUG, INFO, WARN, ERROR. Defaults: DEBUG for external-data mode (excludes TRACE), TRACE for self-contained mode (includes everything).

Why it matters: TRACE-level messages are Robot Framework's most verbose output. In external-data mode with large suites they can represent a significant fraction of payload size. Excluding them by default in external-data mode is a safe trade-off for most users; the flag allows explicit override in either direction.

User-facing behaviour: Messages below the configured level are not written to any file or included in self-contained reports. There is no runtime filtering — it is a generation-time decision.


Compact serialisation (Phase 1 payload optimisation)

What it does: The serialiser now omits fields whose values are falsy: None, False, "", []. Previously every Keyword, Test, and Suite dict was emitted with all fields regardless of value.

Why it matters: In a typical test run the majority of keywords have empty arguments, documentation, messages, and keywords lists. Omitting them reduces per-object payload size materially. The JS template was updated to use (field || []) / (field || "") guards everywhere these fields are consumed.

Concrete impact: The compact serialisation is responsible for most of the pre-compression size reduction. The gzip compression then operates on already-compact JSON, improving the compression ratio further.


3. Improvements

Serialisation payload reduction

Mode ~10k tests (observed) Notes
v0.1.6 external-data (baseline) ~650 MB All fields emitted; TRACE included by default
Phase 1: compact + --loglevel DEBUG ~280 MB Falsy fields omitted; TRACE excluded
Phase 2: --compress-data ~703 MB on disk Dual-write: 280 MB .json + ~20 MB .json.gz
Phase 2: --compress-data-only ~19.7 MB .json.gz only; 33× smaller than v0.1.6

Compression ratio on the JSON data directory: ~33× (from ~650 MB to ~19.7 MB).
The remaining ~0.1 MB is the HTML shell and the reportlens-data/ directory metadata.

Explicit min_log_level propagation

Previously the log level was read from a REPORTLENS_LOG_LEVEL environment variable, making the behaviour implicit, non-composable, and untestable in isolation. It is now a constructor parameter passed explicitly through cli.py → RobotFrameworkReportGenerator → build_report_model. The env-var mechanism is removed entirely.

_write_json_files static helper

Previously file-writing logic was inlined at each call site in _build_external. It is now centralised in a @staticmethod _write_json_files(path_obj, data, compress, gz_only) that handles plain write, dual-write, and gz-only in a single place. The gz_only flag implies compress; the helper documents this contract.

Deterministic fixture isolation

The control_structures_xml_path pytest fixture previously pointed at the project-root output.xml, which is overwritten whenever a benchmark or robot test run executes in the working directory. This caused intermittent test failures depending on which tests had been run most recently. The fixture now points at control_structures_output.xml, a stable checked-in file generated from accounts.robot. The conftest docstring explicitly documents the reason to prevent regression.

report-config schema clarity

Two new boolean fields (compressed, compressedOnly) are added to the embedded config block. Both are only present when True — absent means False. This keeps the config compact and makes backward compatibility trivial (older frontend code reading an absent key gets undefined, coerced to false).

Browser compatibility handling for DecompressionStream

typeof DecompressionStream !== "undefined" is evaluated once at startup and stored in supportsDecompressionStream. All conditional branches that need this check read the constant rather than re-evaluating the type check. The capability guard (compressedOnly && !supportsDecompressionStream) runs before any data fetching begins.


4. Bug Fixes

Duplicated endTime in keyword serialiser

The _keyword_to_dict serialiser emitted endTime twice — once from the explicit field and once from the fallback branch. The second emission overwrote the first with an empty string in cases where end_time was set, producing incorrect data in the rendered report's keyword timing display. Fixed by removing the redundant assignment.

Mutable output.xml fixture contamination

Four TestBuildReportModel tests failed non-deterministically when the benchmark runner or a manual robot invocation had written a different output.xml to the project root between test runs. Fixed by isolating the fixture to control_structures_output.xml.

TRACE log level included in external-data mode by default

Prior to Phase 1, external-data mode included TRACE messages in every per-test JSON payload. This was the primary reason for large payload sizes and was also inconsistent with what most users expect from an external-data / CI-optimised mode. Fixed by defaulting min_log_level to DEBUG in external-data mode. TRACE is still included in self-contained mode (unchanged behaviour) and can be restored in external-data mode with --loglevel TRACE.

Silent blank UI on --compress-data-only in incompatible browsers

Without the capability guard, a browser lacking DecompressionStream would attempt to fetch .json.gz files, receive them as binary, fail JSON parsing on every response, and render a completely blank UI with no error message. Fixed by the early capability guard described in §2.

res.body not cancelled on non-OK gzip response in fetchJsonFile

When the .json.gz fetch returned a non-OK status, the response body stream was left open (no res.body?.cancel() call), holding the TCP connection until GC. This is a known open issue (tracked in PHASE2_REVIEW.md) and is not yet fixed in this release.

Global BUILD_DEBUG env-var side-effect in CLI

os.environ["BUILD_DEBUG"] = "1" was set unconditionally when --debug was passed, leaking into subprocesses. This is an existing known issue; the --debug flag is marked with a TODO in cli.py and is not yet fully implemented.


5. Breaking Changes / Behavioural Changes

Changed default log level in external-data mode ⚠️

Mode v0.1.6 default v0.1.7 default
Self-contained (report.html) TRACE TRACE (unchanged)
External-data (--external-data) TRACE DEBUG

Impact: Users who relied on TRACE messages appearing in external-data reports will need to add --loglevel TRACE explicitly. TRACE messages will no longer appear in external-data reports by default.

This is technically a breaking change to default output content, though most users will not notice because TRACE is Robot Framework's most verbose level and is rarely used in application test suites.

--compress-data writes both formats (dual-write) — not a breaking change

Some users may assume --compress-data replaces the .json files. It does not — it adds .json.gz siblings. Total disk usage roughly doubles before gzip compression benefits are accounted for. Use --compress-data-only for the minimum-size output.

--compress-data-only requires a modern browser — explicit incompatibility

Reports generated with --compress-data-only will not load in:

  • Chrome / Edge < 80
  • Firefox < 113
  • Safari < 16.4
  • Any embedded WebView without DecompressionStream

This is enforced by the capability guard with a clear error banner. It is not a silent failure.

report-config schema additions — backward compatible

New fields compressed and compressedOnly are only present when True. Existing consumers of the config JSON are unaffected.


6. New CLI Options

Option Type Default Requires
--compress-data flag off --external-data
--compress-data-only flag off --external-data
--loglevel choice DEBUG (ext) / TRACE (self-contained)

--compress-data

Purpose: Write a .json.gz sibling for every .json file in reportlens-data/. The HTML report prefers the compressed variant in supporting browsers; old browsers fall back to plain .json.

Default: Off. Opt-in only.

Recommended usage: Default compression option for CI artefact storage where broad browser compatibility is required. The dual-write means disk usage is approximately 2× the uncompressed size before gzip savings are applied (i.e. ~703 MB for 10k tests), but the compressed files themselves are ~20 MB.

Compatibility: Works in all browsers. Old browsers (without DecompressionStream) silently use .json files.


--compress-data-only

Purpose: Write only .json.gz files — no .json fallback. Produces the smallest possible output directory.

Default: Off. Opt-in only.

Recommended usage: Use when you control the browser environment (CI dashboards, internal tools, modern-browser-only deployments). Do not use for public distribution where browser diversity is unknown.

Compatibility: Requires Chrome 80+, Edge 80+, Firefox 113+, Safari 16.4+. Incompatible browsers receive a visible error banner and the report refuses to initialise.

Note: Implies --compress-data internally. Setting both is allowed but redundant.


--loglevel

Purpose: Set the minimum Robot Framework log level to include in the report at generation time.

Choices: TRACE, DEBUG, INFO, WARN, ERROR

Default: DEBUG when --external-data is used; TRACE for self-contained reports.

Recommended usage: --loglevel INFO for the smallest possible payload in production reports. --loglevel TRACE when debugging test failures and needing maximum verbosity. The default (DEBUG) is a reasonable balance for most CI workflows.

Compatibility: No frontend impact. Log level filtering is applied once during XML parsing; messages below the threshold are never written to any output file.


7. Files Changed / Major Internal Refactors

generator.py

What changed:

  • Constructor gains compress_data: bool and compress_data_only: bool parameters. compress_data_only sets self._gz_only = True and implies self._compress_data = True.
  • min_log_level parameter added; default is mode-aware (DEBUG for external-data, TRACE for self-contained). Env-var mechanism removed.
  • _write_json_files(path_obj, data, compress, gz_only) static helper extracted. Handles all three write modes (plain, dual-write, gz-only) in one place.
  • _build_html config block now emits "compressed": True and "compressedOnly": True when applicable.
  • _build_external uses a local write_json closure that delegates to _write_json_files.

Why it changed: Centralising write logic eliminates call-site duplication and makes the three modes independently testable. Explicit constructor parameters make the generator composable without global state.

Architectural significance: generator.py is now a pure function of its constructor arguments. No environment variables, no module-level state. This enables deterministic test isolation.


serialize.py

What changed:

  • _include_value(v) helper introduced: returns False for None, False, "", []. All _*_to_dict functions use this gate before adding a field to the output dict.
  • _keyword_to_dict: removed duplicated endTime emission.
  • _log_message_to_dict: isReturn and isHtml only emitted when True.
  • _test_to_dict_without_messages: uses _include_value gates consistently.

Why it changed: Compact serialisation is the largest single lever for reducing external-data payload size without losing information. Omitting falsy defaults aligns with JSON best practices for sparse objects.

Architectural significance: The serialiser is now responsible for the compact-vs-full decision. Callers do not need to strip fields. The JS template must handle absent fields with || guards — this contract is now documented in README.md.


builder.py

What changed:

  • build_report_model accepts min_log_level: int | None parameter (previously read from env-var).
  • Log messages below min_log_level are filtered during model construction, not at serialisation time.

Why it changed: Filtering at build time means the model itself is clean — no downstream code needs to re-filter, and the memory footprint of the model is proportional to what will actually be written.

Architectural significance: Separates the concern of "what data to collect" from "how to write it". The model is now the authoritative in-memory representation and does not contain data the caller has indicated it does not want.


cli.py

What changed:

  • --compress-data argument added.
  • --compress-data-only argument added.
  • --loglevel argument added with choices=["TRACE","DEBUG","INFO","WARN","ERROR"].
  • min_log_level is now resolved from args.loglevel using _LEVELS.get(...) and passed explicitly to the generator constructor.
  • Both compression flags passed to RobotFrameworkReportGenerator(...).

Why it changed: All new generator capabilities need CLI surface. --loglevel replaces the old REPORTLENS_LOG_LEVEL env-var that was never documented.

Architectural significance: The CLI is now the single place that resolves string level names to integer constants. The generator and builder only ever see integer level values.


template.html

What changed:

  • const compressedOnly = reportConfig.compressedOnly === true; constant added after const compressed.
  • Capability guard block added: if (compressedOnly && !supportsDecompressionStream) — renders error banner, throws to halt initialisation.
  • async function decompressGzipResponse(response) added — uses DecompressionStream("gzip") to pipe-decompress and parse the response body.
  • async function fetchJsonFile(path, maxRetries, timeoutMs) added — wraps fetchWithRetry with a .json.gz preference path when compressed && supportsDecompressionStream.
  • createResourceCache patched to call fetchJsonFile instead of fetchWithRetry.

Why it changed: The frontend must know which fetch strategy to use and must fail loudly rather than silently when the preconditions for compressedOnly are not met.

Architectural significance: All fetch logic is now centralised in fetchJsonFile. Adding a new fetch strategy (e.g. Brotli in future) requires only modifying that one function.


model.py

What changed:

  • All list fields on Keyword, Test, and Suite now have field(default_factory=list) defaults.
  • end_time: str = "" default added to Keyword and Test to avoid None in the payload.
  • badge: str | None = None field added to Keyword for control structure type display (FOR, WHILE, IF, ELSE IF, ELSE, TRY, EXCEPT, FINALLY).

Why it changed: Consistent defaults allow the serialiser's _include_value guard to be applied uniformly without special-casing each field. badge was added in support of the control-structure tree rendering introduced in v0.1.4 but not fully landed in the model until this cycle.

Architectural significance: The model is now a complete, self-describing contract. Callers that construct model objects manually (e.g. in tests) get safe defaults without needing to know the serialiser's omission rules.


test_generator.py

What changed:

  • import gzip added.
  • TestCompressedExternalDataMode class added (10 tests):
    • Dual-write file existence, no-gz in uncompressed mode, decompression integrity, summary.json.gz content correctness, compressed:true config flag, absence of flag in uncompressed/self-contained modes, JS artefact presence (fetchJsonFile, decompressGzipResponse, supportsDecompressionStream, capability guard).
  • TestCompressDataOnlyMode class added (7 tests):
    • No plain .json files written, all expected .json.gz present, all payloads valid JSON, compressed:true in config, compressedOnly:true in config, --compress-data does not set compressedOnly, --compress-data still writes both formats.

Why it changed: Every new code path needs coverage. The compression tests exercise both the Python write logic and the JS config flags. The capability guard test verifies the JS artefact is present in the rendered output.

Architectural significance: Tests are grouped by behaviour class. Each class has a private _gen_* helper to avoid setup duplication. Config flag tests assert on the generated HTML content, keeping the contract between generator and frontend explicit.


test_serialize.py

What changed:

  • test_trace_messages_filtered_when_debug_level added: verifies that TRACE messages are absent from the payload when min_log_level=DEBUG is set.
  • test_html_message_isHtml_in_payload added: verifies isHtml: True is emitted only when html=True on the LogMessage.

Why it changed: The _include_value gate's behaviour for booleans needed explicit test coverage.


conftest.py

What changed:

  • control_structures_xml_path fixture path changed from output.xml (project root, mutable) to control_structures_output.xml (checked-in, stable).
  • Docstring added explicitly warning against reverting to the project-root path.

Why it changed: The project-root output.xml is the default output path of every robot invocation, including the benchmark runner. Any test run or benchmark that uses a different robot suite would silently replace it, corrupting the fixture.


8. Benchmark Results

Test environment

  • Machine: Apple Silicon (macOS)
  • Suite size: 10,000 synthetic test cases
  • Robot Framework output: output.xml generated by benchmark_robot_suite
  • Command: reportlens output.xml -o report.html --external-data [--compress-data|--compress-data-only]

Size benchmarks

Mode Data dir size File count Notes
Self-contained (v0.1.6) N/A (single file) 1 HTML ~50–200 MB for 10k tests as embedded JSON
External-data, uncompressed ~650 MB ~10,053 .json TRACE included (old default)
External-data + compact serialisation + --loglevel DEBUG ~280 MB ~10,053 .json TRACE excluded, falsy fields omitted
--compress-data (dual-write) ~703 MB total ~10,053 .json + ~10,053 .json.gz ~20 MB .gz + ~280 MB .json + HTML
--compress-data-only ~19.7 MB ~10,053 .json.gz Best case; 33× smaller than baseline

Per-file averages (estimated at 10k tests)

File type Avg uncompressed Avg gzip (level 9) Ratio
summary.json ~8 KB ~1.5 KB ~5×
suites.json ~120 KB ~15 KB ~8×
suite_<id>.json ~2 KB ~0.4 KB ~5×
test_<id>.json ~4 KB ~0.6 KB ~7×
test_<id>_logs.json ~22 KB ~2 KB ~11×

Log files compress best because they consist of repetitive structured text. Summary and suite files compress modestly because they are smaller and more structurally varied.

Interpretation

The dominant file category by count and total size is test_<id>_logs.json (one per test). With 10k tests this is 10,000 files averaging ~22 KB each = ~220 MB of the 280 MB total. Compression on these files is the primary driver of the 33× overall ratio.

Remaining bottlenecks

  1. Request count — 10k tests require 10k+ HTTP fetches (one test_<id>.json + one test_<id>_logs.json per test viewed). This is currently unavoidable in the split-file architecture. The browser's parallel fetch queue mitigates this in practice but it represents a meaningful overhead for slow networks.
  2. Two files per test — The current architecture splits test metadata (test_<id>.json) from test logs (test_<id>_logs.json). Merging them into a single file would halve the request count at the cost of increased per-file size.
  3. JSON.parse on the main thread — Large compressed files (e.g. suites.json.gz after decompression) are parsed synchronously on the main thread. This is not yet offloaded to a Worker.
  4. suites.json flat list — All suites are emitted in a single file regardless of depth. For very deep or wide suite trees (50k+ tests) this becomes a latency problem on initial load.

9. Test & Stability Summary

Test counts

Version Total tests Passing
v0.1.6 (pre-session) 53 49 (4 failing)
After fixture fix 53 53
After Phase 1 serialiser tests 55 55
After Phase 2 compression tests 68 68
After capability guard tests 71 71

Newly added tests (this release cycle)

TestCompressedExternalDataMode (11 tests):

  • test_compressed_files_generated_alongside_json
  • test_uncompressed_mode_writes_no_gz_files
  • test_gzip_payload_decompresses_to_valid_json
  • test_summary_gz_has_correct_statistics
  • test_compressed_html_config_has_compressed_true
  • test_uncompressed_html_config_has_no_compressed_flag
  • test_self_contained_report_has_no_compressed_flag
  • test_template_js_contains_fetchJsonFile
  • test_template_js_contains_decompressGzipResponse
  • test_template_js_detects_DecompressionStream
  • test_template_js_has_compressed_only_capability_guard

TestCompressDataOnlyMode (7 tests):

  • test_gz_only_writes_no_plain_json
  • test_gz_only_writes_all_gz_files
  • test_gz_only_payloads_are_valid
  • test_gz_only_html_config_has_compressed_true
  • test_gz_only_html_config_has_compressed_only_true
  • test_compress_data_does_not_set_compressed_only
  • test_compress_data_still_writes_both

TestModelToPayload (2 new tests):

  • test_html_message_isHtml_in_payload
  • test_trace_messages_filtered_when_debug_level

Fixture isolation

control_structures_output.xml — stable checked-in fixture generated from accounts.robot. Replaces the mutable project-root output.xml reference. The conftest docstring documents the reason.

Known remaining test gaps

  • No test covers the fetchJsonFile fallback path when .json.gz returns non-OK (requires a mock HTTP server).
  • No test covers the decompressGzipResponse error path (malformed gzip data).
  • The capability guard banner content is tested by asserting on JS source strings rather than DOM rendering (no headless browser test infrastructure).

10. Known Limitations / Future Work

Request count scaling

With the current per-test-per-logs split, viewing N tests requires up to 2N HTTP fetches. At 10k tests with --compress-data-only the total data transfer is ~20 MB, but the request count (~20,000) can still be a bottleneck on high-latency connections or on HTTP/1.1 servers. Potential fix: merge test_<id>.json and test_<id>_logs.json into a single test_<id>.json file — halves request count, improves gzip ratio (more context per file), simplifies the generator and frontend. This is the highest-impact Phase 3 candidate.

testCache memory growth (no LRU eviction)

dataStore.testCache accumulates every loaded test payload indefinitely for the lifetime of the browser tab. For a 10k-test session where the user browses many tests, this can grow to hundreds of MB of retained heap. Potential fix: cap at ~200 entries using an LRU eviction policy (evict least-recently-used entry on insert when over cap).

JSON.parse on the main thread

After DecompressionStream decompresses a gzip payload, JSON.parse(text) runs synchronously on the main thread. For large files (e.g. a suites.json covering hundreds of suites) this can cause a visible UI jank. Potential fix: use a Worker to perform decompression and parsing off the main thread, posting the result back via postMessage.

suites.json flat list at scale

All suite metadata is written to a single suites.json. This is loaded once on init and is fine up to several thousand suites. Above ~50k tests with deep nesting, the file becomes large enough to be a noticeable initial-load bottleneck. Potential fix: lazy per-suite-level loading (load child suite metadata only on expand, similar to how test data is loaded).

gzip.open mtime not set to 0

The gzip header includes a modification timestamp (mtime) which is set to the current wall clock time by default. This means two identical runs produce byte-for-byte different .json.gz files, breaking CDN content-addressed caching and making diff-based artifact storage less efficient. Fix: gzip.open(gz_path, "wb", compresslevel=9, mtime=0) — trivial one-line change.

res.body not cancelled on non-OK gzip response

In fetchJsonFile, when the .json.gz fetch returns a non-OK HTTP status, the response body stream is not cancelled before falling through to the plain fetch. The connection is held until GC. Fix: if (!res.ok) { res.body?.cancel(); /* fall through */ }.

No console.warn on gzip decompression fallback

When fetchJsonFile catches a decompression error and falls back to plain .json, there is no console.warn. Users investigating network issues in DevTools cannot distinguish "no .gz file" from "decompression failed" from "network error". Fix: add console.warn("ReportLens: gz decompression failed for", gzPath, _, "— falling back to plain JSON") in the catch block.


11. Suggested Release Notes Draft


robotframework-reportlens v0.1.7

Gzip compression for external-data mode, log level control, and serialisation optimisation

Summary

This release makes large-suite external-data reports practical for CI artefact storage. At 10k tests the data directory shrinks from ~650 MB to ~20 MB (33×) using the new --compress-data-only flag. All compression and decompression is client-side; no server configuration is required.

New features

--compress-data — Writes gzip-compressed .json.gz siblings for every .json file in reportlens-data/. The report automatically uses the compressed files in browsers that support the native DecompressionStream API (Chrome 80+, Edge 80+, Firefox 113+, Safari 16.4+), with silent fallback to plain .json on older browsers. Both formats are written, so compatibility is preserved at the cost of roughly double disk usage during generation.

--compress-data-only — Writes only .json.gz files. Produces the smallest possible output (~20 MB for 10k tests vs ~650 MB uncompressed). Reports generated with this flag will not load in browsers without DecompressionStream; an early capability check renders a visible error banner and halts initialisation rather than silently failing. Use when you control the browser environment. Requires --external-data.

--loglevel TRACE|DEBUG|INFO|WARN|ERROR — Sets the minimum log level included in the report at generation time. Defaults to DEBUG in external-data mode (excludes TRACE messages) and TRACE in self-contained mode. Use --loglevel INFO to further reduce payload size in production reports.

Improvements

  • Compact JSON serialisation — Falsy fields (None, False, "", []) are no longer emitted in the JSON payload. Reduces uncompressed data size by approximately 50% at typical test densities.
  • Explicit log level configuration — Log level is now a constructor parameter rather than an environment variable, making the generator composable and deterministic.
  • Browser capability guard--compress-data-only reports detect DecompressionStream availability at startup and show a clear error banner if the browser is unsupported, instead of producing a blank UI with thousands of silent fetch failures.
  • Stable test fixtures — Builder tests now use a checked-in control_structures_output.xml fixture instead of the mutable project-root output.xml, eliminating intermittent test failures caused by benchmark runs.

Breaking changes

  • External-data mode default log level changed from TRACE to DEBUG. TRACE messages are no longer included in external-data reports by default. Add --loglevel TRACE to restore the previous behaviour.
  • Reports generated with --compress-data-only require Chrome 80+, Edge 80+, Firefox 113+, or Safari 16.4+.

Bug fixes

  • Fixed duplicated endTime field in keyword serialiser output.
  • Fixed intermittent test failures caused by fixture contamination from benchmark runs.

Tests

71 tests passing (up from 53 before this cycle). 18 new tests covering: gzip file generation, decompression integrity, report-config flag correctness, JS frontend artefact presence, capability guard, and serialiser compaction rules.