All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.4.0 - 2026-03-27
capture_resources: falseoption onPerchfall.run/Client#run— whentrue, collects metadata for every resource loaded during the page run and stores large ones on the reportreport.resources— array ofResourceobjects (url, http_method, status, content_type, transfer_size, resource_type) whose transfer size met or exceeded the configured threshold, plus any resource whose size could not be determined (absentcontent-length)large_resource_threshold_bytes:option (default 200 000 bytes / 200 KB) — controls the minimum transfer size for a resource to appear inreport.resources; only meaningful whencapture_resources: truePerchfall::Resourcevalue object (Data.define) with all resource fields;transfer_sizeisIntegerornil(nil means unknown, not zero)
- Resource capture is opt-in and off by default. No overhead is incurred unless
capture_resources: trueis passed. - Resources with an unknown transfer size (
nil) are always included inreport.resources— they cannot be proven to be below the threshold. report.ok?is not affected byreport.resources— size is a metric, not a pass/fail signal.
0.3.2 - 2026-03-19
Perchfall.run!— like.runbut raisesPageLoadErrorwhen the report is not ok. Use in scripts or jobs that should abort on any page failure.
Perchfall.runnow always returns aReport, even when the page has unignored errors. Callers checkreport.ok?and handle failures themselves. This makes notification workflows possible without rescuing exceptions.
0.3.1 - 2026-03-19
Report#ok?now returnsfalsewhen there are unignored network or console errors, even if the page reached the load milestone (status == "ok"). Previously,ok?only checkedstatus.PlaywrightInvoker#raise_if_page_load_errornow raisesPageLoadErrorfor reports with unignored errors. ThePageLoadErrorcarries the full report so callers can inspect which errors triggered the failure.- Errors moved to
ignored_network_errors/ignored_console_errorsby anIgnoreRulecontinue to be excluded fromok?— they have been explicitly acknowledged by the caller.
0.2.0 - 2026-03-19
cache_profile:option onPerchfall.run— replacesbust_cache:with four named profiles::query_bust(default) — appends?_pf=<unix_timestamp>to force a cold fetch:warm— no URL mutation, no extra headers; measures real-user warm-cache experience:no_cache— setsCache-Control: no-cacheon all requests (main document + sub-resources):no_store— setsCache-Control: no-store, no-cacheandPragma: no-cache- Custom Hash form:
cache_profile: { headers: { "Cache-Control" => "max-age=0" } }
report.cache_profile— cache profile is stored on theReportand included into_h/to_json--headersargument toplaywright/check.js— extra HTTP headers applied viapage.setExtraHTTPHeaderscheck.jsintegration specs (15 examples, tagged:js); excluded from default run, opt-in viaRUN_JS_SPECS=truecheck-js.ymlGitHub Actions workflow — runs automatically whenplaywright/check.jsor its specs change; caches Playwright Chromium binary keyed onpackage-lock.json
- Renamed cache-bust query parameter from
_perchfall=to_pf=(shorter, less intrusive in logs) - Validation order:
cache_profile→wait_until→timeout_ms→ URL validation — invalid params now raise before the effective URL is built check.jsnow writes astatus: "error"JSON result (exit 0) for malformed or non-object--headersinstead of crashing
bust_cache:keyword argument removed. Migrate:bust_cache: false→cache_profile: :warm;bust_cache: true→cache_profile: :query_bust(or omit — it is the default)- Cache-bust query parameter renamed from
_perchfall=to_pf=— update any log filters or URL allow-lists
- Custom
cache_profileheaders validated against aFORBIDDEN_HEADERSdenylist (Authorization,Cookie,Set-Cookie,Host,X-Forwarded-For,X-Forwarded-Host,X-Real-IP) — these cannot be injected via the custom Hash form
0.1.0 - 2026-03-17
Perchfall.run(url:)— primary public API; returns an immutableReportvalue objectbust_cache:option (defaulttrue) appends a_perchfall=<timestamp>query parameter to prevent CDN and proxy caching from masking real page state;report.urlalways reflects the original caller URL, not the cache-busted onescenario_name:option included in the report for labelling checkswait_until:option (load,domcontentloaded,networkidle,commit) controls when Playwright considers navigation completetimeout_ms:option (default 30 000, max 60 000) for Playwright navigation timeoutReportvalue object withok?,http_status,duration_ms,network_errors,console_errors,to_jsonignored_network_errors/ignored_console_errorsonReport— errors suppressed by ignore rules are captured, not silently dropped- Configurable ignore rules via
ignore:—IgnoreRulesupports substring, regex, and wildcard matching on URL/text and failure/type fields - Default ignore rule suppresses
net::ERR_ABORTED(analytics beacons, cancelled prefetches) - Typed exception hierarchy:
PageLoadError(with partial report),ConcurrencyLimitError,InvocationError,ScriptError,ParseError - Process-wide concurrency limiter (default 5 simultaneous Chromium instances) using Mutex + ConditionVariable — no spinning, slot always released
- SSRF mitigations: scheme allowlist (
http/httpsonly), literal IP blocklist (loopback, link-local, RFC-1918), DNS resolution check; URL validation always runs against the effective URL sent to Playwright (post cache-bust) - Full dependency injection throughout — test suite runs in ~0.4 s with no browser, Node, or network required
- GitHub Actions CI workflow (unit suite) and manual Playwright smoke check workflow