Skip to content

Repository files navigation

Perchfall

CIPlaywright smoke checkGem VersionLicense: MIT

Synthetic browser monitoring for Ruby. Give it a URL; get back a structured report of what a Chromium browser saw — HTTP status, broken assets, JavaScript errors, and load time. No framework required.

report=Perchfall.run(url: "https://example.com")report.ok?# => truereport.http_status# => 200report.duration_ms# => 834report.network_errors# => []report.console_errors# => []report.to_json# => '{"status":"ok","url":"https://example.com",...}'

Why Perchfall

Uptime monitoring tells you a server is responding. Perchfall tells you the page actually works.

  • A 200 OK doesn't mean your JavaScript loaded.
  • An APM trace doesn't capture a missing CDN asset.
  • A health check endpoint doesn't know your checkout flow is broken.

Perchfall runs a headless Chromium browser against your URL and gives you back everything it found: the HTTP status, every failed network request, every JavaScript error logged to the console, and how long it took. The result is an immutable Ruby value object you can store, log, or alert on — no database schema imposed, no framework lock-in.

Drop it into a Sidekiq job, a Rake task, a CI step, or a plain Ruby script. It works anywhere Ruby runs.


Requirements

DependencyVersion
Ruby≥ 3.2
Node≥ 18
Playwrightinstalled via npm

Installation

# 1. Add the gem
bundle add perchfall
# 2. Install Playwright (once per machine)
npm install playwright
npx playwright install chromium

Quickstart

require"perchfall"report=Perchfall.run(url: "https://example.com")ifreport.ok?puts"#{report.url} loaded in #{report.duration_ms}ms"elseputs"Page failed: #{report.network_errors.map(&:failure).join(", ")}"end

Detect broken assets and JS errors

report=Perchfall.run(url: "https://example.com")report.network_errors.eachdo |e|
puts"#{e.http_method}#{e.url}#{e.failure}"end# GET https://example.com/assets/app.js — HTTP 404# GET https://cdn.example.com/font.woff — net::ERR_NAME_NOT_RESOLVEDreport.console_errors.eachdo |e|
puts"#{e.type}: #{e.text}"end# error: Uncaught ReferenceError: Stripe is not defined

A page can be ok: true (it loaded) and still have broken sub-resources. Perchfall captures both.

Handle page load failures

beginreport=Perchfall.run(url: "https://example.com",timeout_ms: 10_000)rescuePerchfall::Errors::PageLoadError=>e# Page couldn't load at all — a partial report is always attached.store_report(e.report.to_json)end

Use in a background job

classSyntheticCheckJobincludeSidekiq::Jobdefperform(url)report=Perchfall.run(url: url)SyntheticResult.create!(ok: report.ok?,payload: report.to_json)rescuePerchfall::Errors::PageLoadError=>eSyntheticResult.create!(ok: false,payload: e.report.to_json)endend

What's in a report

Every check returns a Perchfall::Report:

FieldTypeDescription
ok?Booleantrue if the page loaded successfully
http_statusInteger / nilHTTP response code
duration_msIntegerTotal time from navigation start to load event
urlStringThe URL checked
timestampTimeWhen the check ran (UTC)
cache_profileSymbol / nilCache profile used (:query_bust, :warm, :no_cache, :no_store)
network_errorsArrayFailed or errored network requests
console_errorsArrayJavaScript errors logged to the browser console
to_jsonStringFull report as JSON

Full report schema and JSON reference


Errors

ExceptionWhen
ArgumentErrorURL is invalid (bad scheme, internal address)
Perchfall::Errors::PageLoadErrorPage couldn't load; partial report attached at e.report
Perchfall::Errors::ConcurrencyLimitErrorAll browser slots are busy; back off and retry
Perchfall::Errors::InvocationErrorNode isn't installed or not in PATH
Perchfall::Errors::ErrorBase class — catches any Perchfall error

Full error handling guide


Configuration

Perchfall.run(url: "https://example.com",timeout_ms: 10_000,# default 30_000, max 60_000wait_until: "domcontentloaded",# default "load"scenario_name: "homepage_smoke",# included in report JSONcache_profile: :no_cache,# default :query_bustretries: 2,# default 0 (off); opt-in retry of transient failuresretry_on: [:load_error,:script_error,:server_error],# defaultretry_backoff_ms: 250# default 250 (exponential: 250, 500, 1000…))

Retry transient failures

Small timing blips — a navigation that times out by a hair, a connection reset, a server returning 5xx mid-restart — can fail a check that would pass on a second look. Retries are opt-in; you declare which conditions are worth retrying.

# Up to 2 extra attempts on load/script/5xx failures (the defaults)Perchfall.run(url: "https://example.com",retries: 2)

Load failures, process failures (:script_error), and HTTP 5xx (:server_error) are retried by default; :client_error (4xx) and :network_error are available but off. JavaScript/console (assertion) errors are never retried — they are real defects, not timing blips. A failure is retried only when every reason it failed is a declared condition. ConcurrencyLimitError is not auto-retried — it's a back-pressure signal to back off at the caller level.

All options, cache profiles, retries, and wait_until strategies


Further reading


Development

bundle install
bundle exec rspec # ~0.5s, no browser or Node required (292 examples)
RUN_JS_SPECS=true bundle exec rspec # includes check.js integration specs (310 examples)
bundle exec rubocop # lint; enforced in CI
bin/console # IRB with perchfall loaded

Contributing

Bug reports and pull requests are welcome on GitHub. See CONTRIBUTING.md.

Code of Conduct

Everyone interacting in the Perchfall project's codebases, issue trackers, and discussions is expected to follow the code of conduct.

License

Released under the MIT License.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages