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.
- checksums.yaml +7 -0
- data/.nvmrc +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +47 -0
- data/CODE_OF_CONDUCT.md +83 -0
- data/CONTRIBUTING.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +228 -0
- data/Rakefile +12 -0
- data/app/assets/tailwind/perchfall_rails/engine.css +1 -0
- data/app/controllers/perchfall/rails/synthetic_runs_controller.rb +22 -0
- data/app/jobs/perchfall/rails/run_job.rb +33 -0
- data/app/models/perchfall/rails/synthetic_run.rb +27 -0
- data/app/serializers/perchfall/rails/ignore_rule_serializer.rb +27 -0
- data/app/serializers/perchfall/rails/regexp_serializer.rb +19 -0
- data/app/services/perchfall/rails/synthetic_run/persist.rb +41 -0
- data/app/views/layouts/perchfall/rails/application.html.erb +13 -0
- data/app/views/perchfall/rails/synthetic_runs/_console_error_card.html.erb +14 -0
- data/app/views/perchfall/rails/synthetic_runs/_network_error_card.html.erb +14 -0
- data/app/views/perchfall/rails/synthetic_runs/index.html.erb +63 -0
- data/app/views/perchfall/rails/synthetic_runs/show.html.erb +104 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260315235045_create_synthetic_runs.rb +20 -0
- data/lib/generators/perchfall/rails/install_generator.rb +27 -0
- data/lib/generators/perchfall/rails/templates/initializer.rb +11 -0
- data/lib/perchfall/rails/configuration.rb +22 -0
- data/lib/perchfall/rails/engine.rb +33 -0
- data/lib/perchfall/rails/version.rb +7 -0
- data/lib/perchfall/rails.rb +12 -0
- data/lib/tasks/perchfall/rails.rake +53 -0
- data/package.json +5 -0
- 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
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
+
[](https://github.com/beflagrant/perchfall-rails/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/perchfall-rails)
|
|
5
|
+
[](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 @@
|
|
|
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"] %> — <%= 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"] %> — <%= 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
|
+
← 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,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,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
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: []
|