ReportLens v0.1.7
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: boolandcompress_data_only: boolparameters.compress_data_onlysetsself._gz_only = Trueand impliesself._compress_data = True. min_log_levelparameter added; default is mode-aware (DEBUGfor external-data,TRACEfor 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_htmlconfig block now emits"compressed": Trueand"compressedOnly": Truewhen applicable._build_externaluses a localwrite_jsonclosure 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: returnsFalseforNone,False,"",[]. All_*_to_dictfunctions use this gate before adding a field to the output dict._keyword_to_dict: removed duplicatedendTimeemission._log_message_to_dict:isReturnandisHtmlonly emitted whenTrue._test_to_dict_without_messages: uses_include_valuegates 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_modelacceptsmin_log_level: int | Noneparameter (previously read from env-var).- Log messages below
min_log_levelare 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-dataargument added.--compress-data-onlyargument added.--loglevelargument added withchoices=["TRACE","DEBUG","INFO","WARN","ERROR"].min_log_levelis now resolved fromargs.loglevelusing_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 afterconst compressed.- Capability guard block added:
if (compressedOnly && !supportsDecompressionStream)— renders error banner, throws to halt initialisation. async function decompressGzipResponse(response)added — usesDecompressionStream("gzip")to pipe-decompress and parse the response body.async function fetchJsonFile(path, maxRetries, timeoutMs)added — wrapsfetchWithRetrywith a.json.gzpreference path whencompressed && supportsDecompressionStream.createResourceCachepatched to callfetchJsonFileinstead offetchWithRetry.
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
listfields onKeyword,Test, andSuitenow havefield(default_factory=list)defaults. end_time: str = ""default added toKeywordandTestto avoidNonein the payload.badge: str | None = Nonefield added toKeywordfor 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 gzipadded.TestCompressedExternalDataModeclass added (10 tests):- Dual-write file existence, no-gz in uncompressed mode, decompression integrity,
summary.json.gzcontent correctness,compressed:trueconfig flag, absence of flag in uncompressed/self-contained modes, JS artefact presence (fetchJsonFile,decompressGzipResponse,supportsDecompressionStream, capability guard).
- Dual-write file existence, no-gz in uncompressed mode, decompression integrity,
TestCompressDataOnlyModeclass added (7 tests):- No plain
.jsonfiles written, all expected.json.gzpresent, all payloads valid JSON,compressed:truein config,compressedOnly:truein config,--compress-datadoes not setcompressedOnly,--compress-datastill writes both formats.
- No plain
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_leveladded: verifies thatTRACEmessages are absent from the payload whenmin_log_level=DEBUGis set.test_html_message_isHtml_in_payloadadded: verifiesisHtml: Trueis emitted only whenhtml=Trueon theLogMessage.
Why it changed: The _include_value gate's behaviour for booleans needed explicit test coverage.
conftest.py
What changed:
control_structures_xml_pathfixture 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
- Request count — 10k tests require 10k+ HTTP fetches (one
test_<id>.json+ onetest_<id>_logs.jsonper 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. - 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. JSON.parseon the main thread — Large compressed files (e.g.suites.json.gzafter decompression) are parsed synchronously on the main thread. This is not yet offloaded to a Worker.suites.jsonflat 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_jsontest_uncompressed_mode_writes_no_gz_filestest_gzip_payload_decompresses_to_valid_jsontest_summary_gz_has_correct_statisticstest_compressed_html_config_has_compressed_truetest_uncompressed_html_config_has_no_compressed_flagtest_self_contained_report_has_no_compressed_flagtest_template_js_contains_fetchJsonFiletest_template_js_contains_decompressGzipResponsetest_template_js_detects_DecompressionStreamtest_template_js_has_compressed_only_capability_guard
TestCompressDataOnlyMode (7 tests):
test_gz_only_writes_no_plain_jsontest_gz_only_writes_all_gz_filestest_gz_only_payloads_are_validtest_gz_only_html_config_has_compressed_truetest_gz_only_html_config_has_compressed_only_truetest_compress_data_does_not_set_compressed_onlytest_compress_data_still_writes_both
TestModelToPayload (2 new tests):
test_html_message_isHtml_in_payloadtest_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
fetchJsonFilefallback path when.json.gzreturns non-OK (requires a mock HTTP server). - No test covers the
decompressGzipResponseerror 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-onlyreports detectDecompressionStreamavailability 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
TRACEtoDEBUG.TRACEmessages are no longer included in external-data reports by default. Add--loglevel TRACEto restore the previous behaviour. - Reports generated with
--compress-data-onlyrequire Chrome 80+, Edge 80+, Firefox 113+, or Safari 16.4+.
Bug fixes
- Fixed duplicated
endTimefield 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.