perchfall-rails 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.nvmrc +1 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +47 -0
  5. data/CODE_OF_CONDUCT.md +83 -0
  6. data/CONTRIBUTING.md +40 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +228 -0
  9. data/Rakefile +12 -0
  10. data/app/assets/tailwind/perchfall_rails/engine.css +1 -0
  11. data/app/controllers/perchfall/rails/synthetic_runs_controller.rb +22 -0
  12. data/app/jobs/perchfall/rails/run_job.rb +33 -0
  13. data/app/models/perchfall/rails/synthetic_run.rb +27 -0
  14. data/app/serializers/perchfall/rails/ignore_rule_serializer.rb +27 -0
  15. data/app/serializers/perchfall/rails/regexp_serializer.rb +19 -0
  16. data/app/services/perchfall/rails/synthetic_run/persist.rb +41 -0
  17. data/app/views/layouts/perchfall/rails/application.html.erb +13 -0
  18. data/app/views/perchfall/rails/synthetic_runs/_console_error_card.html.erb +14 -0
  19. data/app/views/perchfall/rails/synthetic_runs/_network_error_card.html.erb +14 -0
  20. data/app/views/perchfall/rails/synthetic_runs/index.html.erb +63 -0
  21. data/app/views/perchfall/rails/synthetic_runs/show.html.erb +104 -0
  22. data/config/routes.rb +5 -0
  23. data/db/migrate/20260315235045_create_synthetic_runs.rb +20 -0
  24. data/lib/generators/perchfall/rails/install_generator.rb +27 -0
  25. data/lib/generators/perchfall/rails/templates/initializer.rb +11 -0
  26. data/lib/perchfall/rails/configuration.rb +22 -0
  27. data/lib/perchfall/rails/engine.rb +33 -0
  28. data/lib/perchfall/rails/version.rb +7 -0
  29. data/lib/perchfall/rails.rb +12 -0
  30. data/lib/tasks/perchfall/rails.rake +53 -0
  31. data/package.json +5 -0
  32. metadata +131 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 66ca38a327877a7cbae38ccda37e0c621fd3bb6b1aa2076c4d36fcf6fa849ab4
4
+ data.tar.gz: daad94c0e0867f968b13bcf6b54b6e850df851b61f884a0c74cfe70524ea3cf2
5
+ SHA512:
6
+ metadata.gz: 5285413f55fd9d32c236e17b13c0d54904cebb9f3e7ce4bb0eefcbc1fcc11845c868004b5289a71bf92e33bea0a435e8cad8525baa626070807215e97508497f
7
+ data.tar.gz: 268ac26f7a97ad1d1c081a27d87bbec73762a5685f1b16797fd0278c3261707f5df323ae5cede4517ed655b7866c2e0e85b70d6ec908c87e00e3f57a6e83c250
data/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 22
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.1] - 2026-05-05
11
+
12
+ ### Added
13
+
14
+ - `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1) and `CONTRIBUTING.md`.
15
+ - README badges (CI, Gem Version, License) and a "Why perchfall-rails" section.
16
+ - Gemspec metadata: `bug_tracker_uri`, `rubygems_mfa_required`, and a `post_install_message` pointing at `bin/rails perchfall_rails:install:playwright`.
17
+
18
+ ### Changed
19
+
20
+ - LICENSE copyright holder standardized to Flagrant LLC, matching the rest of the perchfall family.
21
+
22
+ ## [0.2.0] - 2026-05-05
23
+
24
+ ### Added
25
+
26
+ - `RegexpSerializer` and `IgnoreRuleSerializer` — custom `ActiveJob::Serializers::ObjectSerializer` subclasses registered at engine boot via `config.to_prepare`. `RunJob` now supports `perform_later` with `ignore:`; callers no longer need to choose between async execution and ignore rules. `RegexpSerializer` applies to any job that passes a `Regexp` argument. See [ADR 0002](docs/adr/0002-ignore-rule-serializer.md).
27
+
28
+ ## [0.1.0] - 2026-05-04
29
+
30
+ ### Added
31
+
32
+ - Mountable Rails engine under the `Perchfall::Rails` namespace. Mount at any path with `mount Perchfall::Rails::Engine, at: "/perchfall"`.
33
+ - `SyntheticRun` ActiveRecord model persisting every check result with status (`ok`, `failed`, or `error`), HTTP status, duration, denormalized error counts, and the full raw report as JSON.
34
+ - `RunJob` ActiveJob subclass that runs a Perchfall check and persists the result. Supports `url:`, `scenario_name:`, and `ignore:` (via `perform_now` only — `IgnoreRule` objects are not ActiveJob-serializable). Failed runs persist an error record and re-raise so the queue adapter can retry.
35
+ - Dashboard with a paginated, filterable index (filter by status or URL) and a detail view showing network and console errors.
36
+ - Configuration API via `Perchfall::Rails.configure`: `base_controller` (default: `"ApplicationController"`) and `queue_name` (default: `:synthetic`).
37
+ - Production boot warning when `base_controller` is still `ApplicationController`.
38
+ - `rails generate perchfall:rails:install` generator that creates `config/initializers/perchfall_rails.rb` and prints setup instructions.
39
+ - `perchfall_rails:install:playwright` Rake task to install the Playwright npm package and Chromium browser binary.
40
+ - `perchfall_rails:check` Rake task to verify Node, the Playwright package, and the Chromium binary are all present and usable.
41
+ - Tailwind CSS integration: semantic token names (`primary`, `muted`, `subtle`, `error`, `border-subtle`, `neutral-0/150/900`) in all views. Tailwind v4 hosts import `perchfall_rails/engine` to scan the engine's views automatically; Tailwind v3 hosts add the engine view path to their `content` configuration.
42
+ - Runtime dependencies: `perchfall ~> 0.4`, `pagy ~> 43.0`, Rails `>= 8.0`.
43
+
44
+ [Unreleased]: https://github.com/beflagrant/perchfall-rails/compare/v0.2.1...HEAD
45
+ [0.2.1]: https://github.com/beflagrant/perchfall-rails/compare/v0.2.0...v0.2.1
46
+ [0.2.0]: https://github.com/beflagrant/perchfall-rails/compare/v0.1.0...v0.2.0
47
+ [0.1.0]: https://github.com/beflagrant/perchfall-rails/releases/tag/v0.1.0
@@ -0,0 +1,83 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or advances of any kind
22
+ * Trolling, insulting or derogatory comments, and personal or political attacks
23
+ * Public or private harassment
24
+ * Publishing others' private information, such as a physical or email address, without their explicit permission
25
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
26
+
27
+ ## Enforcement Responsibilities
28
+
29
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
32
+
33
+ ## Scope
34
+
35
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
36
+
37
+ ## Enforcement
38
+
39
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening a GitHub issue. All complaints will be reviewed and investigated promptly and fairly.
40
+
41
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
42
+
43
+ ## Enforcement Guidelines
44
+
45
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
46
+
47
+ ### 1. Correction
48
+
49
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
50
+
51
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
52
+
53
+ ### 2. Warning
54
+
55
+ **Community Impact**: A violation through a single incident or series of actions.
56
+
57
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
58
+
59
+ ### 3. Temporary Ban
60
+
61
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
62
+
63
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
64
+
65
+ ### 4. Permanent Ban
66
+
67
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
68
+
69
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
70
+
71
+ ## Attribution
72
+
73
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
74
+
75
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
76
+
77
+ For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
78
+
79
+ [homepage]: https://www.contributor-covenant.org
80
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
81
+ [Mozilla CoC]: https://github.com/mozilla/diversity
82
+ [FAQ]: https://www.contributor-covenant.org/faq
83
+ [translations]: https://www.contributor-covenant.org/translations
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,40 @@
1
+ # Contributing to perchfall-rails
2
+
3
+ Thank you for your interest in contributing. All skill levels are welcome — whether this is your first open source contribution or your hundredth.
4
+
5
+ ## Before you start
6
+
7
+ **Open an issue first.** Before writing any code, please open a GitHub issue describing what you want to do and why. This saves everyone time: it avoids duplicate work, surfaces design concerns early, and gives us a chance to agree on scope before you invest effort in an implementation.
8
+
9
+ The exception is typos and documentation fixes, which can go straight to a PR.
10
+
11
+ ## Proposing a significant change
12
+
13
+ For anything that changes behavior, adds a new concept, or touches security — please propose it as a GitHub Discussion before opening an issue or PR. Structure your proposal like an ADR:
14
+
15
+ - **Context** — what problem are you solving, and why does it matter?
16
+ - **Options considered** — what alternatives did you weigh?
17
+ - **Decision** — what are you proposing, and why?
18
+ - **Consequences** — what does this make easier or harder?
19
+
20
+ You can look at the existing ADRs in [docs/adr/](docs/adr/) for examples of this format. Discussion lets the proposal get feedback before any code is written, and the thread becomes useful context for the eventual PR.
21
+
22
+ ## Submitting a pull request
23
+
24
+ - Tests are required. If you're adding behavior, cover it. If you're fixing a bug, add a test that would have caught it.
25
+ - Rubocop must pass. Run `bundle exec rubocop` before pushing.
26
+ - Keep commits focused. One logical change per commit is easier to review and revert if needed.
27
+ - Update the CHANGELOG under `[Unreleased]` following the existing format.
28
+
29
+ ## Running the suite
30
+
31
+ ```sh
32
+ bin/setup
33
+ bundle exec rake # runs the test suite and rubocop
34
+ ```
35
+
36
+ The test suite uses a dummy Rails app under `test/dummy` and does not require Node, Playwright, or a browser.
37
+
38
+ ## Code of conduct
39
+
40
+ This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold it.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flagrant LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # perchfall-rails
2
+
3
+ [![CI](https://github.com/beflagrant/perchfall-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/beflagrant/perchfall-rails/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/perchfall-rails.svg)](https://badge.fury.io/rb/perchfall-rails)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A mountable Rails engine that adds a synthetic monitoring dashboard and background job pipeline to any Rails application, powered by the [perchfall](https://github.com/beflagrant/perchfall) gem.
8
+
9
+ ## Why perchfall-rails
10
+
11
+ [perchfall](https://github.com/beflagrant/perchfall) gives you a `Report` value object — what a real Chromium browser saw at a URL. It deliberately doesn't impose a database schema or a job queue; you bring your own.
12
+
13
+ perchfall-rails is the batteries-included path for Rails apps:
14
+
15
+ - **Schema and persistence** — a `SyntheticRun` ActiveRecord model that stores every check, with denormalized error counts and the full report JSON for later inspection.
16
+ - **Background job** — a `Perchfall::Rails::RunJob` ActiveJob subclass you can `perform_later` (including with `ignore:` rules), with sensible retry behavior.
17
+ - **Dashboard** — paginated, filterable index of runs and a detail view that surfaces every network and console error.
18
+ - **Auth-agnostic** — point `base_controller` at any controller in your app and inherit its authentication.
19
+
20
+ If you only need the Ruby API, use perchfall directly. If you want a Rails-idiomatic place for synthetic results to live, mount this.
21
+
22
+ ## Requirements
23
+
24
+ - Ruby >= 3.3
25
+ - Rails >= 8.0
26
+ - Node.js >= 18 (required by perchfall for Playwright)
27
+ - [pagy](https://github.com/ddnexus/pagy) ~> 43.0 (bundled as a dependency — no separate install needed)
28
+
29
+ ## Installation
30
+
31
+ Add to your Gemfile:
32
+
33
+ ```ruby
34
+ gem "perchfall-rails"
35
+ ```
36
+
37
+ Then run:
38
+
39
+ ```sh
40
+ bundle install
41
+ ```
42
+
43
+ ### Node.js and Playwright
44
+
45
+ The engine requires Node.js and the Playwright npm package with a Chromium browser binary. After bundling:
46
+
47
+ ```sh
48
+ bin/rails perchfall_rails:install:playwright
49
+ ```
50
+
51
+ This runs `npm install playwright` and `npx playwright install chromium`. Verify the full environment with:
52
+
53
+ ```sh
54
+ bin/rails perchfall_rails:check
55
+ ```
56
+
57
+ ### Initializer
58
+
59
+ Generate the configuration initializer:
60
+
61
+ ```sh
62
+ bin/rails generate perchfall:rails:install
63
+ ```
64
+
65
+ This creates `config/initializers/perchfall_rails.rb` and prints the remaining setup steps.
66
+
67
+ ### Migrations
68
+
69
+ Install and run the engine migrations:
70
+
71
+ ```sh
72
+ bin/rails perchfall_rails:install:migrations
73
+ bin/rails db:migrate
74
+ ```
75
+
76
+ ### Routes
77
+
78
+ Mount the engine in `config/routes.rb`:
79
+
80
+ ```ruby
81
+ mount Perchfall::Rails::Engine, at: "/perchfall"
82
+ ```
83
+
84
+ The dashboard will be available at `/perchfall/synthetic_runs`.
85
+
86
+ ## Configuration
87
+
88
+ Create an initializer at `config/initializers/perchfall_rails.rb`:
89
+
90
+ ```ruby
91
+ Perchfall::Rails.configure do |config|
92
+ # Required in production: set this to a controller that enforces authentication.
93
+ # The engine will log a warning at boot if this is still ApplicationController
94
+ # in a production environment.
95
+ config.base_controller = "AuthenticatedController"
96
+
97
+ # ActiveJob queue name for synthetic run jobs. Defaults to :synthetic.
98
+ config.queue_name = :synthetic
99
+ end
100
+ ```
101
+
102
+ ## Authentication
103
+
104
+ The engine has no built-in authentication. Set `base_controller` to a controller that enforces whatever authentication your application uses:
105
+
106
+ ```ruby
107
+ # config/initializers/perchfall_rails.rb
108
+ Perchfall::Rails.configure do |config|
109
+ config.base_controller = "Admin::BaseController"
110
+ end
111
+ ```
112
+
113
+ `base_controller` must inherit from `ActionController::Base`. The engine views use `csrf_meta_tags`, `csp_meta_tag`, and `stylesheet_link_tag`, none of which are available on `ActionController::API` controllers.
114
+
115
+ ## Enqueueing checks
116
+
117
+ Enqueue a synthetic run from anywhere in your application:
118
+
119
+ ```ruby
120
+ Perchfall::Rails::RunJob.perform_later(url: "https://example.com")
121
+ ```
122
+
123
+ To suppress known-noisy errors, pass `ignore:` rules:
124
+
125
+ ```ruby
126
+ rule = Perchfall::IgnoreRule.new(
127
+ pattern: "chrome-error://",
128
+ type: "*",
129
+ target: :console
130
+ )
131
+ Perchfall::Rails::RunJob.perform_later(
132
+ url: "https://example.com",
133
+ scenario_name: "homepage",
134
+ ignore: [rule]
135
+ )
136
+ ```
137
+
138
+ **Retry behavior:** When a run fails with a `Perchfall::Errors::Error`, the job persists an error record and then re-raises the exception so the queue adapter can retry normally. Each retry attempt that also fails will create an additional error record. If you want to cap error records at one per enqueued job, configure `discard_on` in a subclass or use your queue adapter's retry settings:
139
+
140
+ ```ruby
141
+ class MyRunJob < Perchfall::Rails::RunJob
142
+ discard_on Perchfall::Errors::Error
143
+ end
144
+ ```
145
+
146
+ ## Tailwind CSS
147
+
148
+ The engine views use semantic Tailwind token names. Add the following to your Tailwind configuration to define the required tokens:
149
+
150
+ **Tailwind v4**:
151
+
152
+ `tailwindcss-rails` automatically generates a bridge file at `app/assets/builds/tailwind/perchfall_rails.css` when the engine is bundled. Import it from your Tailwind CSS entry point, then define the required tokens. Replace the example values with your application's actual colors:
153
+
154
+ ```css
155
+ @import "tailwindcss";
156
+ @import "../builds/tailwind/perchfall_rails";
157
+
158
+ @theme {
159
+ --color-primary: #2563eb;
160
+ --color-muted: #6b7280;
161
+ --color-subtle: #9ca3af;
162
+ --color-error: #ef4444;
163
+ --color-border-subtle: #e5e7eb;
164
+
165
+ --color-neutral-0: #ffffff;
166
+ --color-neutral-150: #e5e7eb;
167
+ --color-neutral-900: #111827;
168
+ }
169
+ ```
170
+
171
+ The `../builds/tailwind/perchfall_rails` path assumes your CSS entry point is in `app/assets/tailwind/` or `app/assets/stylesheets/`. Adjust if your layout differs.
172
+
173
+ **Tailwind v3** (`tailwind.config.js`):
174
+
175
+ Add the engine's views to your content paths and define the required tokens. Replace the example values with your application's actual colors:
176
+
177
+ ```js
178
+ const { execSync } = require("child_process");
179
+ const enginePath = execSync("bundle show perchfall-rails").toString().trim();
180
+
181
+ module.exports = {
182
+ content: [
183
+ // ... your existing content paths
184
+ `${enginePath}/app/views/**/*.html.erb`,
185
+ ],
186
+ theme: {
187
+ extend: {
188
+ colors: {
189
+ primary: "#2563eb",
190
+ muted: "#6b7280",
191
+ subtle: "#9ca3af",
192
+ error: "#ef4444",
193
+ neutral: {
194
+ 0: "#ffffff",
195
+ 150: "#e5e7eb",
196
+ 900: "#111827",
197
+ },
198
+ },
199
+ borderColor: {
200
+ subtle: "#e5e7eb",
201
+ },
202
+ },
203
+ },
204
+ }
205
+ ```
206
+
207
+ If your application does not use Tailwind, the views render unstyled but functional.
208
+
209
+ ## Development
210
+
211
+ After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests.
212
+
213
+ ```sh
214
+ bin/setup
215
+ bundle exec rake test
216
+ ```
217
+
218
+ ## Contributing
219
+
220
+ Bug reports and pull requests are welcome on GitHub at https://github.com/beflagrant/perchfall-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/beflagrant/perchfall-rails/blob/main/CODE_OF_CONDUCT.md).
221
+
222
+ ## License
223
+
224
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
225
+
226
+ ## Code of Conduct
227
+
228
+ Everyone interacting in the perchfall-rails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/beflagrant/perchfall-rails/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1 @@
1
+ @source "../../../views/**/*.html.erb";
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class SyntheticRunsController < Perchfall::Rails.configuration.base_controller.constantize
6
+ include ::Pagy::Method
7
+
8
+ def index
9
+ runs = SyntheticRun.recent
10
+ runs = runs.for_status(params[:status]) if SyntheticRun::STATUSES.include?(params[:status])
11
+ runs = runs.for_url(params[:url]) if params[:url].present?
12
+ @pagy, @runs = pagy(:offset, runs)
13
+ end
14
+
15
+ def show
16
+ @run = SyntheticRun.find(params[:id])
17
+ rescue ActiveRecord::RecordNotFound
18
+ head :not_found
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class RunJob < ActiveJob::Base
6
+ queue_as { Perchfall::Rails.configuration.queue_name }
7
+
8
+ # NOTE: ignore: cannot be used with perform_later — IgnoreRule objects are not
9
+ # ActiveJob-serializable. Use perform_now when passing ignore rules.
10
+ def perform(url:, scenario_name: nil, ignore: [], client: nil)
11
+ client ||= Perchfall::Client.new
12
+ report = client.run(url: url, scenario_name: scenario_name, ignore: ignore)
13
+ SyntheticRun::Persist.call(report, url: url, scenario_name: scenario_name)
14
+ rescue Perchfall::Errors::PageLoadError => e
15
+ SyntheticRun.create!(
16
+ url: url,
17
+ scenario_name: scenario_name,
18
+ status: "error",
19
+ raw_report: e.report.to_h
20
+ )
21
+ raise
22
+ rescue Perchfall::Errors::Error => e
23
+ SyntheticRun.create!(
24
+ url: url,
25
+ scenario_name: scenario_name,
26
+ status: "error",
27
+ raw_report: { "error" => e.message }
28
+ )
29
+ raise
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class SyntheticRun < ActiveRecord::Base
6
+ self.table_name = "synthetic_runs"
7
+
8
+ STATUSES = %w[ok failed error].freeze
9
+
10
+ validates :url, presence: true
11
+ validates :status, presence: true, inclusion: { in: STATUSES }
12
+
13
+ scope :recent, -> { order(created_at: :desc) }
14
+ scope :failures, -> { where(status: %w[failed error]) }
15
+ scope :for_url, ->(url) { where(url: url) }
16
+ scope :for_status, ->(status) { where(status: status) }
17
+
18
+ def ok? = status == "ok"
19
+ def failed? = !ok?
20
+
21
+ def network_errors = raw_report["network_errors"] || []
22
+ def console_errors = raw_report["console_errors"] || []
23
+ def ignored_network_errors = raw_report["ignored_network_errors"] || []
24
+ def ignored_console_errors = raw_report["ignored_console_errors"] || []
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class IgnoreRuleSerializer < ActiveJob::Serializers::ObjectSerializer
6
+ def klass
7
+ Perchfall::IgnoreRule
8
+ end
9
+
10
+ def serialize(rule)
11
+ super(
12
+ "pattern" => ActiveJob::Arguments.serialize_argument(rule.pattern),
13
+ "type" => ActiveJob::Arguments.serialize_argument(rule.type),
14
+ "target" => ActiveJob::Arguments.serialize_argument(rule.target)
15
+ )
16
+ end
17
+
18
+ def deserialize(hash)
19
+ Perchfall::IgnoreRule.new(
20
+ pattern: ActiveJob::Arguments.deserialize([hash["pattern"]]).first,
21
+ type: ActiveJob::Arguments.deserialize([hash["type"]]).first,
22
+ target: ActiveJob::Arguments.deserialize([hash["target"]]).first
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class RegexpSerializer < ActiveJob::Serializers::ObjectSerializer
6
+ def klass
7
+ Regexp
8
+ end
9
+
10
+ def serialize(regexp)
11
+ super("source" => regexp.source, "options" => regexp.options)
12
+ end
13
+
14
+ def deserialize(hash)
15
+ Regexp.new(hash["source"], hash["options"])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class SyntheticRun
6
+ class Persist
7
+ def self.call(report, url:, scenario_name: nil)
8
+ new(report, url: url, scenario_name: scenario_name).call
9
+ end
10
+
11
+ def initialize(report, url:, scenario_name:)
12
+ @report = report
13
+ @url = url
14
+ @scenario_name = scenario_name
15
+ end
16
+
17
+ def call
18
+ SyntheticRun.create!(
19
+ url: @url,
20
+ scenario_name: @scenario_name,
21
+ status: map_status,
22
+ http_status: @report.http_status,
23
+ duration_ms: @report.duration_ms,
24
+ network_error_count: @report.network_errors.size,
25
+ console_error_count: @report.console_errors.size,
26
+ raw_report: @report.to_h
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def map_status
33
+ return "error" if @report.status == "error"
34
+ return "failed" if @report.network_errors.any? || @report.console_errors.any?
35
+
36
+ "ok"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Perchfall</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag :app %>
9
+ </head>
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,14 @@
1
+ <div class="p-8 border border-solid border-neutral-150 rounded-2xl bg-neutral-0 flex flex-col gap-4">
2
+ <h3 class="text-3xl font-extrabold leading-[120%] <%= heading_color %>">
3
+ <%= title %> <span class="text-2xl font-semibold <%= count_color %>">(<%= count %>)</span>
4
+ </h3>
5
+ <% errors.each_with_index do |err, i| %>
6
+ <div class="flex flex-col gap-1 <%= 'opacity-60' if dimmed %>">
7
+ <span class="text-xl font-normal text-neutral-900 break-all"><%= err["text"] %></span>
8
+ <span class="text-sm font-normal text-muted"><%= err["type"] %> &mdash; <%= err["location"] %></span>
9
+ </div>
10
+ <% unless i == errors.size - 1 %>
11
+ <hr class="border-subtle">
12
+ <% end %>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="p-8 border border-solid border-neutral-150 rounded-2xl bg-neutral-0 flex flex-col gap-4">
2
+ <h3 class="text-3xl font-extrabold leading-[120%] <%= heading_color %>">
3
+ <%= title %> <span class="text-2xl font-semibold <%= count_color %>">(<%= count %>)</span>
4
+ </h3>
5
+ <% errors.each_with_index do |err, i| %>
6
+ <div class="flex flex-col gap-1 <%= 'opacity-60' if dimmed %>">
7
+ <span class="text-xl font-normal text-neutral-900 break-all"><%= err["url"] %></span>
8
+ <span class="text-sm font-normal text-muted"><%= err["method"] %> &mdash; <%= err["failure"] %></span>
9
+ </div>
10
+ <% unless i == errors.size - 1 %>
11
+ <hr class="border-subtle">
12
+ <% end %>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,63 @@
1
+ <div class="flex flex-col gap-6 w-full">
2
+ <div class="flex flex-col md:flex-row items-start md:items-center self-stretch justify-between gap-4">
3
+ <h2 class="text-5xl font-extrabold leading-[120%] text-primary">Synthetic Runs</h2>
4
+
5
+ <%= form_with url: synthetic_runs_path, method: :get, class: "flex flex-row gap-3 items-center" do |f| %>
6
+ <%= f.select :status,
7
+ Perchfall::Rails::SyntheticRun::STATUSES.map { |status| [status.capitalize, status] },
8
+ { include_blank: "All statuses", selected: params[:status] },
9
+ class: "text-sm font-medium text-muted border border-neutral-150 rounded-lg px-3 py-2 bg-neutral-0" %>
10
+ <%= f.text_field :url,
11
+ placeholder: "Filter by URL",
12
+ value: params[:url],
13
+ class: "text-sm text-neutral-900 border border-neutral-150 rounded-lg px-3 py-2 bg-neutral-0 w-64" %>
14
+ <%= f.submit "Filter",
15
+ class: "text-sm font-semibold text-primary border border-neutral-150 rounded-lg px-4 py-2 bg-neutral-0 cursor-pointer" %>
16
+ <% end %>
17
+ </div>
18
+
19
+ <div class="p-8 border border-solid border-neutral-150 rounded-2xl bg-neutral-0 flex flex-col gap-6">
20
+ <div class="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_1fr] gap-4 items-center">
21
+ <span class="text-lg font-semibold leading-[140%] text-muted">URL / Scenario</span>
22
+ <span class="text-lg font-semibold leading-[140%] text-muted">Status</span>
23
+ <span class="text-lg font-semibold leading-[140%] text-muted text-right">HTTP</span>
24
+ <span class="text-lg font-semibold leading-[140%] text-muted text-right">Duration</span>
25
+ <span class="text-lg font-semibold leading-[140%] text-muted text-right">Net errs</span>
26
+ <span class="text-lg font-semibold leading-[140%] text-muted text-right">When</span>
27
+ </div>
28
+
29
+ <hr class="border-subtle">
30
+
31
+ <% if @runs.none? %>
32
+ <p class="text-xl font-normal text-muted">No runs found.</p>
33
+ <% end %>
34
+
35
+ <% @runs.each_with_index do |run, i| %>
36
+ <div class="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_1fr] gap-4 items-center">
37
+ <div class="flex flex-col gap-0.5">
38
+ <%= link_to synthetic_run_path(run), class: "text-xl font-normal leading-[140%] text-primary underline truncate" do %>
39
+ <%= run.scenario_name || run.url %>
40
+ <% end %>
41
+ <% if run.scenario_name.present? %>
42
+ <span class="text-sm font-normal text-muted"><%= run.url %></span>
43
+ <% end %>
44
+ </div>
45
+
46
+ <span class="<%= run.ok? ? 'text-primary' : 'text-error' %> text-xl font-semibold leading-[140%]">
47
+ <%= run.status.capitalize %>
48
+ </span>
49
+
50
+ <span class="text-xl font-normal leading-[140%] text-neutral-900 text-right"><%= run.http_status || "—" %></span>
51
+ <span class="text-xl font-normal leading-[140%] text-neutral-900 text-right"><%= run.duration_ms ? "#{run.duration_ms}ms" : "—" %></span>
52
+ <span class="text-xl font-normal leading-[140%] text-neutral-900 text-right"><%= run.network_error_count %></span>
53
+ <span class="text-xl font-normal leading-[140%] text-muted text-right"><%= time_ago_in_words(run.created_at) %> ago</span>
54
+ </div>
55
+
56
+ <% unless i == @runs.size - 1 %>
57
+ <hr class="border-subtle">
58
+ <% end %>
59
+ <% end %>
60
+ </div>
61
+
62
+ <%== @pagy.series_nav %>
63
+ </div>
@@ -0,0 +1,104 @@
1
+ <div class="flex flex-col gap-6 w-full">
2
+ <div class="flex flex-row items-center gap-4">
3
+ <%= link_to synthetic_runs_path, class: "text-muted hover:text-primary text-lg w-full" do %>
4
+ &larr; All runs
5
+ <% end %>
6
+ </div>
7
+ <div class="flex flex-row items-center gap-4">
8
+ <h2 class="text-5xl font-extrabold leading-[120%] text-primary w-full">
9
+ <%= @run.scenario_name.presence || @run.url %>
10
+ </h2>
11
+ </div>
12
+
13
+ <div class="flex flex-col md:flex-row gap-4 self-stretch items-start">
14
+ <%# Summary card %>
15
+ <div class="p-8 border border-solid border-neutral-150 rounded-2xl bg-neutral-0 flex flex-col gap-6 flex-1">
16
+ <h3 class="text-3xl font-extrabold leading-[120%] text-primary">Summary</h3>
17
+
18
+ <dl class="flex flex-col gap-4">
19
+ <div class="grid grid-cols-2 gap-4">
20
+ <dt class="text-lg font-semibold text-muted">Status</dt>
21
+ <dd class="text-xl font-semibold <%= @run.ok? ? 'text-primary' : 'text-error' %>">
22
+ <%= @run.status.capitalize %>
23
+ </dd>
24
+ </div>
25
+ <hr class="border-subtle">
26
+ <div class="grid grid-cols-2 gap-4">
27
+ <dt class="text-lg font-semibold text-muted">Scenario</dt>
28
+ <dd class="text-xl font-normal text-neutral-900 break-all"><%= @run.scenario_name %></dd>
29
+ </div>
30
+ <hr class="border-subtle">
31
+ <% if @run.scenario_name.present? %>
32
+ <div class="grid grid-cols-2 gap-4">
33
+ <dt class="text-lg font-semibold text-muted">URL</dt>
34
+ <dd class="text-xl font-normal text-neutral-900"><%= @run.url %></dd>
35
+ </div>
36
+ <hr class="border-subtle">
37
+ <% end %>
38
+ <div class="grid grid-cols-2 gap-4">
39
+ <dt class="text-lg font-semibold text-muted">HTTP status</dt>
40
+ <dd class="text-xl font-normal text-neutral-900"><%= @run.http_status || "—" %></dd>
41
+ </div>
42
+ <hr class="border-subtle">
43
+ <div class="grid grid-cols-2 gap-4">
44
+ <dt class="text-lg font-semibold text-muted">Duration</dt>
45
+ <dd class="text-xl font-normal text-neutral-900"><%= @run.duration_ms ? "#{@run.duration_ms}ms" : "—" %></dd>
46
+ </div>
47
+ <hr class="border-subtle">
48
+ <div class="grid grid-cols-2 gap-4">
49
+ <dt class="text-lg font-semibold text-muted">Run at</dt>
50
+ <dd class="text-xl font-normal text-neutral-900"><%= @run.created_at.strftime("%b %-d, %Y at %H:%M UTC") %></dd>
51
+ </div>
52
+ </dl>
53
+ </div>
54
+
55
+ <div class="flex flex-col gap-4 flex-1">
56
+ <% if @run.network_errors.any? %>
57
+ <%= render "network_error_card",
58
+ errors: @run.network_errors,
59
+ title: "Network Errors",
60
+ count: @run.network_error_count,
61
+ heading_color: "text-primary",
62
+ count_color: "text-error",
63
+ dimmed: false %>
64
+ <% end %>
65
+
66
+ <% if @run.console_errors.any? %>
67
+ <%= render "console_error_card",
68
+ errors: @run.console_errors,
69
+ title: "Console Errors",
70
+ count: @run.console_error_count,
71
+ heading_color: "text-primary",
72
+ count_color: "text-error",
73
+ dimmed: false %>
74
+ <% end %>
75
+
76
+ <% if @run.network_errors.none? && @run.console_errors.none? %>
77
+ <div class="p-8 border border-solid border-neutral-150 rounded-2xl bg-neutral-0 flex flex-col gap-2">
78
+ <h3 class="text-3xl font-extrabold leading-[120%] text-primary">No errors</h3>
79
+ <p class="text-xl font-normal text-muted">This run completed cleanly.</p>
80
+ </div>
81
+ <% end %>
82
+
83
+ <% if @run.ignored_network_errors.any? %>
84
+ <%= render "network_error_card",
85
+ errors: @run.ignored_network_errors,
86
+ title: "Ignored Network Errors",
87
+ count: @run.ignored_network_errors.size,
88
+ heading_color: "text-muted",
89
+ count_color: "text-subtle",
90
+ dimmed: true %>
91
+ <% end %>
92
+
93
+ <% if @run.ignored_console_errors.any? %>
94
+ <%= render "console_error_card",
95
+ errors: @run.ignored_console_errors,
96
+ title: "Ignored Console Errors",
97
+ count: @run.ignored_console_errors.size,
98
+ heading_color: "text-muted",
99
+ count_color: "text-subtle",
100
+ dimmed: true %>
101
+ <% end %>
102
+ </div>
103
+ </div>
104
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Perchfall::Rails::Engine.routes.draw do
4
+ resources :synthetic_runs, only: %i[index show]
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSyntheticRuns < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :synthetic_runs do |t|
6
+ t.string :url, null: false
7
+ t.string :scenario_name
8
+ t.string :status, null: false
9
+ t.integer :http_status
10
+ t.integer :duration_ms
11
+ t.integer :network_error_count, null: false, default: 0
12
+ t.integer :console_error_count, null: false, default: 0
13
+ t.json :raw_report, null: false, default: {}
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :synthetic_runs, :status
18
+ add_index :synthetic_runs, %i[url created_at]
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def copy_initializer
9
+ template "initializer.rb", "config/initializers/perchfall_rails.rb"
10
+ end
11
+
12
+ def show_instructions
13
+ say ""
14
+ say "Next steps:", :green
15
+ say " 1. Mount the engine in config/routes.rb:"
16
+ say ' mount Perchfall::Rails::Engine, at: "/perchfall"'
17
+ say " 2. Install and run the engine migrations:"
18
+ say " bin/rails perchfall_rails:install:migrations"
19
+ say " bin/rails db:migrate"
20
+ say " 3. Install Node.js dependencies:"
21
+ say " bin/rails perchfall_rails:install:playwright"
22
+ say " 4. Set base_controller in the generated initializer before deploying to production."
23
+ say ""
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Perchfall::Rails.configure do |config|
4
+ # Set this to a controller that enforces authentication before deploying to production.
5
+ # Must inherit from ActionController::Base (not ActionController::API).
6
+ # The engine logs a warning at boot if this is still ApplicationController in production.
7
+ config.base_controller = "ApplicationController"
8
+
9
+ # ActiveJob queue name for synthetic run jobs. Defaults to :synthetic.
10
+ # config.queue_name = :synthetic
11
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class Configuration
6
+ attr_accessor :base_controller, :queue_name
7
+
8
+ def initialize
9
+ @base_controller = "ApplicationController"
10
+ @queue_name = :synthetic
11
+ end
12
+ end
13
+
14
+ def self.configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def self.configure
19
+ yield configuration
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Perchfall::Rails
7
+
8
+ config.after_initialize do
9
+ if ::Rails.env.production? &&
10
+ Perchfall::Rails.configuration.base_controller == "ApplicationController"
11
+ ::Rails.logger.warn(
12
+ "[perchfall-rails] base_controller is still ApplicationController. " \
13
+ "The synthetic runs dashboard is publicly accessible. " \
14
+ "Set config.base_controller to a controller that enforces authentication."
15
+ )
16
+ end
17
+ end
18
+
19
+ config.to_prepare do
20
+ ActiveJob::Serializers.add_serializers(
21
+ Perchfall::Rails::RegexpSerializer,
22
+ Perchfall::Rails::IgnoreRuleSerializer
23
+ )
24
+ end
25
+
26
+ initializer "perchfall.node_path" do
27
+ local_node_modules = ::Rails.root.join("node_modules").to_s
28
+ existing = ENV["NODE_PATH"].to_s
29
+ ENV["NODE_PATH"] = [local_node_modules, existing].reject(&:empty?).join(":")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Rails
5
+ VERSION = "0.2.1"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pagy"
4
+ require "perchfall"
5
+ require_relative "rails/version"
6
+ require_relative "rails/configuration"
7
+ require_relative "rails/engine"
8
+
9
+ module Perchfall
10
+ module Rails
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :perchfall_rails do
4
+ namespace :install do
5
+ desc "Install Node.js dependencies required by Perchfall (playwright npm package + Chromium)"
6
+ task :playwright do
7
+ system("npm install playwright") || abort("FAIL npm install playwright failed")
8
+ system("npx playwright install chromium") || abort("FAIL npx playwright install chromium failed")
9
+ puts "\nPlaywright and Chromium installed successfully."
10
+ end
11
+ end
12
+
13
+ desc "Check that Node and the playwright npm package are installed and usable"
14
+ task check: :environment do
15
+ # 1. Node
16
+ node_result = Perchfall::CommandRunner.new.call(["node", "--version"])
17
+ if node_result.success?
18
+ puts "node: #{node_result.stdout.strip}"
19
+ else
20
+ abort "FAIL node not found or not executable"
21
+ end
22
+
23
+ # 2. playwright npm package resolvable from project node_modules
24
+ local_node_modules = Rails.root.join("node_modules").to_s
25
+ resolve_result = Perchfall::CommandRunner.new.call(
26
+ ["node", "-e", "require('playwright'); console.log(require('playwright/package.json').version)"]
27
+ )
28
+ if resolve_result.success?
29
+ puts "playwright: #{resolve_result.stdout.strip} (#{local_node_modules})"
30
+ else
31
+ warn resolve_result.stderr.strip
32
+ abort "FAIL playwright npm package not found — run: rails perchfall_rails:install:playwright"
33
+ end
34
+
35
+ # 3. Chromium browser binary present
36
+ chromium_result = Perchfall::CommandRunner.new.call(
37
+ ["node", "-e", <<~JS]
38
+ const { chromium } = require('playwright');
39
+ chromium.launch({ headless: true })
40
+ .then(b => { console.log('chromium ok'); b.close(); })
41
+ .catch(e => { process.stderr.write(e.message + '\\n'); process.exit(1); });
42
+ JS
43
+ )
44
+ if chromium_result.success?
45
+ puts "chromium: #{chromium_result.stdout.strip}"
46
+ else
47
+ warn chromium_result.stderr.strip
48
+ abort "FAIL chromium browser not launchable — run: rails perchfall_rails:install:playwright"
49
+ end
50
+
51
+ puts "\nAll checks passed. Perchfall/Playwright is ready."
52
+ end
53
+ end
data/package.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "playwright": "^1.58.2"
4
+ }
5
+ }
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perchfall-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Yossef Mendelssohn
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pagy
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '43.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '43.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: perchfall
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.4'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rails
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '8.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '8.0'
54
+ description: Mountable Rails engine that provides a background job pipeline, ActiveRecord
55
+ model, controller, and views for running and reviewing Perchfall synthetic browser
56
+ checks.
57
+ email:
58
+ - ymendel@pobox.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".nvmrc"
64
+ - ".ruby-version"
65
+ - CHANGELOG.md
66
+ - CODE_OF_CONDUCT.md
67
+ - CONTRIBUTING.md
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - app/assets/tailwind/perchfall_rails/engine.css
72
+ - app/controllers/perchfall/rails/synthetic_runs_controller.rb
73
+ - app/jobs/perchfall/rails/run_job.rb
74
+ - app/models/perchfall/rails/synthetic_run.rb
75
+ - app/serializers/perchfall/rails/ignore_rule_serializer.rb
76
+ - app/serializers/perchfall/rails/regexp_serializer.rb
77
+ - app/services/perchfall/rails/synthetic_run/persist.rb
78
+ - app/views/layouts/perchfall/rails/application.html.erb
79
+ - app/views/perchfall/rails/synthetic_runs/_console_error_card.html.erb
80
+ - app/views/perchfall/rails/synthetic_runs/_network_error_card.html.erb
81
+ - app/views/perchfall/rails/synthetic_runs/index.html.erb
82
+ - app/views/perchfall/rails/synthetic_runs/show.html.erb
83
+ - config/routes.rb
84
+ - db/migrate/20260315235045_create_synthetic_runs.rb
85
+ - lib/generators/perchfall/rails/install_generator.rb
86
+ - lib/generators/perchfall/rails/templates/initializer.rb
87
+ - lib/perchfall/rails.rb
88
+ - lib/perchfall/rails/configuration.rb
89
+ - lib/perchfall/rails/engine.rb
90
+ - lib/perchfall/rails/version.rb
91
+ - lib/tasks/perchfall/rails.rake
92
+ - package.json
93
+ homepage: https://github.com/beflagrant/perchfall-rails
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ allowed_push_host: https://rubygems.org
98
+ homepage_uri: https://github.com/beflagrant/perchfall-rails
99
+ source_code_uri: https://github.com/beflagrant/perchfall-rails
100
+ changelog_uri: https://github.com/beflagrant/perchfall-rails/blob/main/CHANGELOG.md
101
+ bug_tracker_uri: https://github.com/beflagrant/perchfall-rails/issues
102
+ rubygems_mfa_required: 'true'
103
+ post_install_message: |
104
+ perchfall-rails is installed. To finish setup:
105
+
106
+ bin/rails generate perchfall:rails:install
107
+ bin/rails perchfall_rails:install:playwright
108
+ bin/rails perchfall_rails:install:migrations
109
+ bin/rails db:migrate
110
+
111
+ Then mount the engine in config/routes.rb:
112
+
113
+ mount Perchfall::Rails::Engine, at: "/perchfall"
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: 3.3.0
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 4.0.6
129
+ specification_version: 4
130
+ summary: Rails engine for integrating Perchfall synthetic monitoring
131
+ test_files: []