perchfall-rails 0.2.1 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +58 -1
- data/README.md +77 -114
- data/app/assets/stylesheets/perchfall/rails/application.css +258 -0
- data/app/controllers/perchfall/rails/synthetic_runs_controller.rb +46 -2
- data/app/jobs/perchfall/rails/run_job.rb +3 -13
- data/app/services/perchfall/rails/synthetic_run/persist.rb +38 -2
- data/app/views/layouts/perchfall/rails/application.html.erb +9 -2
- data/app/views/perchfall/rails/synthetic_runs/_console_error_card.html.erb +7 -7
- data/app/views/perchfall/rails/synthetic_runs/_network_error_card.html.erb +7 -7
- data/app/views/perchfall/rails/synthetic_runs/index.html.erb +27 -28
- data/app/views/perchfall/rails/synthetic_runs/show.html.erb +44 -44
- data/lib/generators/perchfall/rails/templates/initializer.rb +30 -0
- data/lib/perchfall/rails/configuration.rb +4 -1
- data/lib/perchfall/rails/engine.rb +17 -0
- data/lib/perchfall/rails/version.rb +1 -1
- metadata +7 -7
- data/.nvmrc +0 -1
- data/.ruby-version +0 -1
- data/app/assets/tailwind/perchfall_rails/engine.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43b1de1c9380bf8b7d9b87e2c9e921840da81511ed2432457131780f9a9b2c4b
|
|
4
|
+
data.tar.gz: 89091a050cbc5c9c666fcbfb68840679c1de88c7c76ea9c28b49cd8635ec574f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6ca9dc520fcd29169b2d47d996432fbcfe9f1c4b1d8ba36c53e265e56c99bfaf53aeff8e8554eb0d261c3637c643f6b8ab170f74eb61914a9b02e3ea0366318
|
|
7
|
+
data.tar.gz: eb3d8ccb62d55e3d853d56d88e00809f9d1fe3e58a979c24221460fa43928943ba4cbd241801f4caadd196802f0d81b21b09901c4dfe740be44548c93417b2a1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-06-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`config.after_persist`** — optional callable invoked once per persisted `Perchfall::Rails::SyntheticRun`. Fires on the happy path *and* `RunJob`'s error rescues; receives the persisted run as a single positional argument. Exceptions raised by the hook are rescued (both `StandardError` and `ScriptError`, so a host `NotImplementedError` can't supplant the engine's own error) and logged with the run id and a partial backtrace — they do not interrupt control flow or prevent ActiveJob retries. See [ADR 0006](docs/adr/0006-synthetic-run-after-persist-hook.md).
|
|
15
|
+
- **`config.synthetic_run_extension`** — optional string naming a host module that the engine constantizes and includes into `Perchfall::Rails::SyntheticRun`. Use this to declare host-side associations, scopes, or callbacks (e.g. `belongs_to :user`) instead of reopening the class from a `config.to_prepare` block. Applied from the engine's own `config.to_prepare`, which fires after host initializers, after eager_load, and again on every code reload (dev). See [ADR 0007](docs/adr/0007-synthetic-run-extension.md).
|
|
16
|
+
- **`synthetic_runs_scope` override hook on `SyntheticRunsController`** — hosts define the method on their `base_controller` to scope the index and show actions (e.g. `where(user: current_user)`). The engine dispatches into it via `respond_to?(:synthetic_runs_scope, true)`, so DSL-materialized methods (method_missing-based delegation) are detected and unrelated same-named methods elsewhere in the ancestry don't get mistaken for the seam. Authorization-by-scoping falls out of the design: `show` 404s for records outside the host's scope without a separate authorization layer. The seam validates the host's return value (raises a self-diagnosing error if nil or non-relation) and normalizes ordering (applies `recent` if the host's relation has no `order_values`), so "always validated, always ordered" applies to every consumer of the seam, not just `index`. See [ADR 0005](docs/adr/0005-synthetic-runs-controller-scope-hook.md).
|
|
17
|
+
- **[ADR 0004](docs/adr/0004-engine-extension-pattern.md)** articulates the engine's host-extension model — "install-and-go defaults with named override seams" — and partially backfills 0.1.0's `base_controller` and 0.3.0's `overrides_stylesheet` under the same principle. Names four established seam shapes and what's explicitly out of bounds (`config.to_prepare` monkey-patches, `Module#prepend` from initializers, wholesale engine class replacement).
|
|
18
|
+
- Install generator template now exposes all five configuration attributes, including the two new ones (`after_persist`, `synthetic_run_extension`). Generator test asserts every attribute appears in the template.
|
|
19
|
+
|
|
20
|
+
### Breaking changes
|
|
21
|
+
|
|
22
|
+
- `SyntheticRun::Persist.call` renamed to `Persist.from_report`, with a new companion `Persist.from_error` that `RunJob`'s rescues route through. Persistence is now consolidated in one place, which is what makes `after_persist` fire for error-path runs. The class-method API is undocumented and the engine's own `RunJob` is updated in this release, but any host calling `Persist.call` directly will see `NoMethodError` post-upgrade and must update to `Persist.from_report`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `Perchfall::Rails::Configuration` attribute order alphabetized to keep future additions consistent.
|
|
27
|
+
|
|
28
|
+
## [0.3.0] - 2026-05-16
|
|
29
|
+
|
|
30
|
+
### Breaking changes
|
|
31
|
+
|
|
32
|
+
- Engine no longer depends on Tailwind CSS or the consumer's design tokens. The dashboard now ships with its own self-contained stylesheet that works on any Rails app. Hosts using the previous Tailwind bridge file (`app/assets/builds/tailwind/perchfall_rails.css`) must remove the `@import` and the `@theme { --color-* }` token block. To customize colors, configure `Perchfall::Rails.configuration.overrides_stylesheet` and define `--perchfall-*` CSS custom properties. See the README "Theming" section for migration steps.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `Perchfall::Rails::Configuration#overrides_stylesheet` — optional path to a host-provided stylesheet loaded after the engine's CSS, for theming via `--perchfall-*` CSS custom properties.
|
|
37
|
+
- Dark mode via `prefers-color-scheme: dark` — engine stylesheet defines both light and dark token sets.
|
|
38
|
+
- Hand-authored engine stylesheet at `app/assets/stylesheets/perchfall/rails/application.css` served through the host's asset pipeline (Propshaft or Sprockets).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Engine layout now loads its own stylesheet (`perchfall/rails/application`) instead of the host's `:app` bundle, isolating dashboard styles from host CSS. Hosts that need full chrome (admin nav, branding) can override the layout by placing their own `app/views/layouts/perchfall/rails/application.html.erb`.
|
|
43
|
+
- All engine views use namespaced `.pf-*` utility classes instead of bare Tailwind utilities.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `SyntheticRunsController` now declares `layout "perchfall/rails/application"` explicitly. Without this, Rails fell back to the host's `layouts/application` and dropped the engine layout entirely — no `<!DOCTYPE>`, no stylesheet link, no `.pf-root` wrapper.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- `app/assets/tailwind/perchfall_rails/` bridge file. Tailwind is no longer a supported integration path.
|
|
52
|
+
- `docs/tailwind-v3.md`.
|
|
53
|
+
|
|
54
|
+
## [0.2.2] - 2026-05-05
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- Gemspec author email updated to `yossef@beflagrant.com`.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- `.nvmrc` and `.ruby-version` no longer bundled in the published gem.
|
|
63
|
+
|
|
10
64
|
## [0.2.1] - 2026-05-05
|
|
11
65
|
|
|
12
66
|
### Added
|
|
@@ -41,7 +95,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
95
|
- 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
96
|
- Runtime dependencies: `perchfall ~> 0.4`, `pagy ~> 43.0`, Rails `>= 8.0`.
|
|
43
97
|
|
|
44
|
-
[Unreleased]: https://github.com/beflagrant/perchfall-rails/compare/v0.
|
|
98
|
+
[Unreleased]: https://github.com/beflagrant/perchfall-rails/compare/v0.4.0...HEAD
|
|
99
|
+
[0.4.0]: https://github.com/beflagrant/perchfall-rails/compare/v0.3.0...v0.4.0
|
|
100
|
+
[0.3.0]: https://github.com/beflagrant/perchfall-rails/compare/v0.2.2...v0.3.0
|
|
101
|
+
[0.2.2]: https://github.com/beflagrant/perchfall-rails/compare/v0.2.1...v0.2.2
|
|
45
102
|
[0.2.1]: https://github.com/beflagrant/perchfall-rails/compare/v0.2.0...v0.2.1
|
|
46
103
|
[0.2.0]: https://github.com/beflagrant/perchfall-rails/compare/v0.1.0...v0.2.0
|
|
47
104
|
[0.1.0]: https://github.com/beflagrant/perchfall-rails/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -8,14 +8,15 @@ A mountable Rails engine that adds a synthetic monitoring dashboard and backgrou
|
|
|
8
8
|
|
|
9
9
|
## Why perchfall-rails
|
|
10
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
|
|
11
|
+
[perchfall](https://github.com/beflagrant/perchfall) gives you a `Report` value object — what a real Chromium browser saw at a URL. It deliberately does not impose a database schema or job queue; you bring your own.
|
|
12
12
|
|
|
13
|
-
perchfall-rails
|
|
13
|
+
perchfall-rails ships the Rails pieces:
|
|
14
14
|
|
|
15
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
16
|
- **Background job** — a `Perchfall::Rails::RunJob` ActiveJob subclass you can `perform_later` (including with `ignore:` rules), with sensible retry behavior.
|
|
17
17
|
- **Dashboard** — paginated, filterable index of runs and a detail view that surfaces every network and console error.
|
|
18
18
|
- **Auth-agnostic** — point `base_controller` at any controller in your app and inherit its authentication.
|
|
19
|
+
- **Extensible** — host-facing seams let you add associations, scope the dashboard to the current user, and react to each persisted run without monkey-patching the engine. See [ADR 0004](docs/adr/0004-engine-extension-pattern.md).
|
|
19
20
|
|
|
20
21
|
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
|
|
|
@@ -23,119 +24,122 @@ If you only need the Ruby API, use perchfall directly. If you want a Rails-idiom
|
|
|
23
24
|
|
|
24
25
|
- Ruby >= 3.3
|
|
25
26
|
- Rails >= 8.0
|
|
26
|
-
- Node.js >= 18 (
|
|
27
|
-
- [pagy](https://github.com/ddnexus/pagy) ~> 43.0 (bundled as a dependency
|
|
27
|
+
- Node.js >= 18 with the `playwright` npm package and a Chromium binary (installed via the engine's rake task — see below)
|
|
28
|
+
- [pagy](https://github.com/ddnexus/pagy) ~> 43.0 (bundled as a dependency)
|
|
28
29
|
|
|
29
30
|
## Installation
|
|
30
31
|
|
|
31
|
-
Add to your Gemfile
|
|
32
|
+
Add to your `Gemfile`:
|
|
32
33
|
|
|
33
34
|
```ruby
|
|
34
35
|
gem "perchfall-rails"
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Install the gem, the initializer, the engine migrations, and Playwright:
|
|
38
39
|
|
|
39
40
|
```sh
|
|
40
41
|
bundle install
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
The engine requires Node.js and the Playwright npm package with a Chromium browser binary. After bundling:
|
|
46
|
-
|
|
47
|
-
```sh
|
|
42
|
+
bin/rails generate perchfall:rails:install
|
|
43
|
+
bin/rails perchfall_rails:install:migrations
|
|
44
|
+
bin/rails db:migrate
|
|
48
45
|
bin/rails perchfall_rails:install:playwright
|
|
49
46
|
```
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
Mount the engine in `config/routes.rb`:
|
|
52
49
|
|
|
53
|
-
```
|
|
54
|
-
|
|
50
|
+
```ruby
|
|
51
|
+
mount Perchfall::Rails::Engine, at: "/perchfall"
|
|
55
52
|
```
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Generate the configuration initializer:
|
|
54
|
+
Verify the Node/Playwright/Chromium environment:
|
|
60
55
|
|
|
61
56
|
```sh
|
|
62
|
-
bin/rails
|
|
57
|
+
bin/rails perchfall_rails:check
|
|
63
58
|
```
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
The dashboard is now available at `/perchfall/synthetic_runs`.
|
|
66
61
|
|
|
67
|
-
|
|
62
|
+
## Quick start
|
|
68
63
|
|
|
69
|
-
|
|
64
|
+
Enqueue a synthetic run from anywhere in your application:
|
|
70
65
|
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
```ruby
|
|
67
|
+
Perchfall::Rails::RunJob.perform_later(url: "https://example.com")
|
|
68
|
+
# => #<Perchfall::Rails::RunJob ...>
|
|
74
69
|
```
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
When the job runs, it creates one `SyntheticRun` record with the full Chromium report, network errors, and console errors. View it at `/perchfall/synthetic_runs`.
|
|
77
72
|
|
|
78
|
-
|
|
73
|
+
To suppress known-noisy errors, pass `ignore:` rules:
|
|
79
74
|
|
|
80
75
|
```ruby
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
rule = Perchfall::IgnoreRule.new(
|
|
77
|
+
pattern: "chrome-error://",
|
|
78
|
+
type: "*",
|
|
79
|
+
target: :console
|
|
80
|
+
)
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
Perchfall::Rails::RunJob.perform_later(
|
|
83
|
+
url: "https://example.com",
|
|
84
|
+
scenario_name: "homepage",
|
|
85
|
+
ignore: [rule]
|
|
86
|
+
)
|
|
87
|
+
```
|
|
85
88
|
|
|
86
89
|
## Configuration
|
|
87
90
|
|
|
88
|
-
|
|
91
|
+
The install generator creates `config/initializers/perchfall_rails.rb` with `base_controller` set to `"ApplicationController"` (with a warning logged at boot in production until you change it) and the optional attributes pre-commented with sample values. A configured initializer looks like:
|
|
89
92
|
|
|
90
93
|
```ruby
|
|
91
94
|
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
95
|
config.base_controller = "AuthenticatedController"
|
|
96
96
|
|
|
97
|
-
#
|
|
98
|
-
config.
|
|
97
|
+
# config.queue_name = :synthetic
|
|
98
|
+
# config.overrides_stylesheet = "perchfall_rails_overrides"
|
|
99
|
+
# config.after_persist = ->(run) { SomeJob.perform_later(run.id) if run.failed? }
|
|
100
|
+
# config.synthetic_run_extension = "SyntheticRunExtension"
|
|
99
101
|
end
|
|
100
102
|
```
|
|
101
103
|
|
|
104
|
+
- `base_controller` — controller the engine inherits from. Required in production; the engine logs a warning at boot if this is still `ApplicationController` in a production environment. Must inherit from `ActionController::Base`.
|
|
105
|
+
- `queue_name` — ActiveJob queue used by `RunJob`. Defaults to `:synthetic`.
|
|
106
|
+
- `overrides_stylesheet` — optional path (without extension) to a host stylesheet loaded after the engine CSS. Use to override `--perchfall-*` CSS custom properties.
|
|
107
|
+
- `after_persist` — optional callable invoked after each `SyntheticRun` is persisted. Receives the run as a single positional argument and fires on both the happy path and `RunJob`'s error rescues — once per persisted run, which under ActiveJob retries means once per attempt. Use `run.id` to route downstream work to that specific persisted run (`SomeJob.perform_later(run.id)`) — each retry attempt persists a distinct id, so `run.id` does **not** dedup across attempts. The engine provides no cross-attempt key today, so handlers that need once-per-failure-event semantics (notifications, ticket-opens) should be designed to be idempotent on the host's natural keys, or should accept at-least-once delivery. Exceptions raised by the callable are rescued and logged; they do not interrupt the pipeline. See [ADR 0006](docs/adr/0006-synthetic-run-after-persist-hook.md).
|
|
108
|
+
- `synthetic_run_extension` — optional name of a host module the engine includes into `Perchfall::Rails::SyntheticRun` from a `config.to_prepare` block (so the include fires after host initializers, after eager_load in production, and again after every code reload in development). Use it to declare host-side associations, scopes, or callbacks (`belongs_to :user`, &c.). The named module must be autoloadable. Callbacks added through the extension run as part of the engine's `SyntheticRun.create!`; if one raises, the create fails, the job dies, no `SyntheticRun` is written (not even an error row), and `after_persist` does not fire. Reserve extension callbacks for declarations and logic that has been hardened against raising; for observability-style side effects, use `after_persist`. See [ADR 0007](docs/adr/0007-synthetic-run-extension.md).
|
|
109
|
+
|
|
102
110
|
## Authentication
|
|
103
111
|
|
|
104
112
|
The engine has no built-in authentication. Set `base_controller` to a controller that enforces whatever authentication your application uses:
|
|
105
113
|
|
|
106
114
|
```ruby
|
|
107
|
-
# config/initializers/perchfall_rails.rb
|
|
108
115
|
Perchfall::Rails.configure do |config|
|
|
109
116
|
config.base_controller = "Admin::BaseController"
|
|
110
117
|
end
|
|
111
118
|
```
|
|
112
119
|
|
|
113
|
-
`base_controller` must inherit from `ActionController::Base`.
|
|
120
|
+
`base_controller` must inherit from `ActionController::Base`. Engine views use `csrf_meta_tags`, `csp_meta_tag`, and `stylesheet_link_tag`, none of which are available on `ActionController::API` controllers.
|
|
114
121
|
|
|
115
|
-
##
|
|
122
|
+
## Scoping runs to the current user
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
By default the dashboard shows every `SyntheticRun` in the database. To scope it — e.g. each user only sees their own runs — define `synthetic_runs_scope` on `base_controller`:
|
|
118
125
|
|
|
119
126
|
```ruby
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
class AuthenticatedController < ApplicationController
|
|
128
|
+
before_action :authenticate_user!
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
private
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
target: :console
|
|
130
|
-
)
|
|
131
|
-
Perchfall::Rails::RunJob.perform_later(
|
|
132
|
-
url: "https://example.com",
|
|
133
|
-
scenario_name: "homepage",
|
|
134
|
-
ignore: [rule]
|
|
135
|
-
)
|
|
132
|
+
def synthetic_runs_scope
|
|
133
|
+
current_user.synthetic_runs
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
The engine calls `synthetic_runs_scope` from both `index` and `show`. If the host returns an unordered relation, the engine merges in a default `recent` order so pagination stays stable. Pair this with two association declarations on the host side: a `belongs_to :user` on `Perchfall::Rails::SyntheticRun` declared via `config.synthetic_run_extension` (which includes the host module into the engine's model — see [ADR 0007](docs/adr/0007-synthetic-run-extension.md)), and the matching `has_many :synthetic_runs, class_name: "Perchfall::Rails::SyntheticRun"` directly on your host model (e.g. `User`). The `synthetic_run_extension` seam includes its module into `SyntheticRun`, so it can only carry the `belongs_to` side; `has_many` belongs on whichever host model owns the association. See [ADR 0005](docs/adr/0005-synthetic-runs-controller-scope-hook.md).
|
|
139
|
+
|
|
140
|
+
## Retry behavior
|
|
141
|
+
|
|
142
|
+
When a run fails with `Perchfall::Errors::Error`, the job persists an error record and re-raises so the queue adapter can retry normally. Each retry that also fails creates another error record. To cap error records at one per enqueued job, subclass `RunJob` and `discard_on`:
|
|
139
143
|
|
|
140
144
|
```ruby
|
|
141
145
|
class MyRunJob < Perchfall::Rails::RunJob
|
|
@@ -143,73 +147,32 @@ class MyRunJob < Perchfall::Rails::RunJob
|
|
|
143
147
|
end
|
|
144
148
|
```
|
|
145
149
|
|
|
146
|
-
##
|
|
150
|
+
## Theming
|
|
147
151
|
|
|
148
|
-
The engine
|
|
152
|
+
The engine ships its own stylesheet. Default colors render well in both light and dark mode (the dashboard follows `prefers-color-scheme`).
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
To customize colors, point `config.overrides_stylesheet` at a host CSS file:
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
```ruby
|
|
157
|
+
Perchfall::Rails.configure do |config|
|
|
158
|
+
config.overrides_stylesheet = "perchfall_rails_overrides"
|
|
159
|
+
end
|
|
160
|
+
```
|
|
153
161
|
|
|
154
162
|
```css
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
163
|
+
/* app/assets/stylesheets/perchfall_rails_overrides.css */
|
|
164
|
+
:root {
|
|
165
|
+
--perchfall-color-primary: #db2777;
|
|
166
|
+
--perchfall-color-error: #b91c1c;
|
|
168
167
|
}
|
|
169
168
|
```
|
|
170
169
|
|
|
171
|
-
|
|
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
|
-
```
|
|
170
|
+
Available tokens are defined in [`app/assets/stylesheets/perchfall/rails/application.css`](app/assets/stylesheets/perchfall/rails/application.css). Headline tokens: `--perchfall-color-primary`, `--perchfall-color-error`, `--perchfall-color-text`, `--perchfall-color-muted`, `--perchfall-color-subtle`, `--perchfall-color-surface`, `--perchfall-color-surface-alt`, `--perchfall-color-border`. The file also exposes typography, spacing, and radius tokens.
|
|
206
171
|
|
|
207
|
-
|
|
172
|
+
Hosts that need full chrome around the dashboard (admin nav, branding) can override the engine layout by placing their own `app/views/layouts/perchfall/rails/application.html.erb` in the host application.
|
|
208
173
|
|
|
209
174
|
## Development
|
|
210
175
|
|
|
211
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the tests.
|
|
212
|
-
|
|
213
176
|
```sh
|
|
214
177
|
bin/setup
|
|
215
178
|
bundle exec rake test
|
|
@@ -217,12 +180,12 @@ bundle exec rake test
|
|
|
217
180
|
|
|
218
181
|
## Contributing
|
|
219
182
|
|
|
220
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/beflagrant/perchfall-rails
|
|
183
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/beflagrant/perchfall-rails>. See [CONTRIBUTING.md](CONTRIBUTING.md). Release notes live in [CHANGELOG.md](CHANGELOG.md).
|
|
221
184
|
|
|
222
185
|
## License
|
|
223
186
|
|
|
224
|
-
The gem is available as open source under the terms of the [MIT License](
|
|
187
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
225
188
|
|
|
226
189
|
## Code of Conduct
|
|
227
190
|
|
|
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](
|
|
191
|
+
Everyone interacting in the perchfall-rails project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/* ===================================================================
|
|
2
|
+
perchfall-rails engine styles
|
|
3
|
+
Hand-authored. Override colors via --perchfall-* CSS custom
|
|
4
|
+
properties in a host stylesheet referenced through
|
|
5
|
+
`config.overrides_stylesheet`.
|
|
6
|
+
=================================================================== */
|
|
7
|
+
|
|
8
|
+
/* -------------------------------------------------------------------
|
|
9
|
+
1. Tokens (light)
|
|
10
|
+
------------------------------------------------------------------- */
|
|
11
|
+
:root {
|
|
12
|
+
--perchfall-color-primary: #2563eb;
|
|
13
|
+
--perchfall-color-error: #ef4444;
|
|
14
|
+
--perchfall-color-text: #111827;
|
|
15
|
+
--perchfall-color-muted: #6b7280;
|
|
16
|
+
--perchfall-color-subtle: #9ca3af;
|
|
17
|
+
--perchfall-color-surface: #ffffff;
|
|
18
|
+
--perchfall-color-surface-alt: #f9fafb;
|
|
19
|
+
--perchfall-color-border: #e5e7eb;
|
|
20
|
+
|
|
21
|
+
--perchfall-font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
22
|
+
--perchfall-font-size-sm: 0.875rem;
|
|
23
|
+
--perchfall-font-size-base: 1rem;
|
|
24
|
+
--perchfall-font-size-lg: 1.125rem;
|
|
25
|
+
--perchfall-font-size-xl: 1.25rem;
|
|
26
|
+
--perchfall-font-size-2xl: 1.5rem;
|
|
27
|
+
--perchfall-font-size-3xl: 1.875rem;
|
|
28
|
+
--perchfall-font-size-5xl: 3rem;
|
|
29
|
+
--perchfall-line-height-tight: 1.2;
|
|
30
|
+
--perchfall-line-height-normal: 1.4;
|
|
31
|
+
|
|
32
|
+
--perchfall-space-0-5: 0.125rem;
|
|
33
|
+
--perchfall-space-1: 0.25rem;
|
|
34
|
+
--perchfall-space-2: 0.5rem;
|
|
35
|
+
--perchfall-space-3: 0.75rem;
|
|
36
|
+
--perchfall-space-4: 1rem;
|
|
37
|
+
--perchfall-space-6: 1.5rem;
|
|
38
|
+
--perchfall-space-8: 2rem;
|
|
39
|
+
|
|
40
|
+
--perchfall-radius-lg: 0.5rem;
|
|
41
|
+
--perchfall-radius-2xl: 1rem;
|
|
42
|
+
|
|
43
|
+
--perchfall-container-max: 1280px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* -------------------------------------------------------------------
|
|
47
|
+
2. Tokens (dark)
|
|
48
|
+
------------------------------------------------------------------- */
|
|
49
|
+
@media (prefers-color-scheme: dark) {
|
|
50
|
+
:root {
|
|
51
|
+
--perchfall-color-primary: #60a5fa;
|
|
52
|
+
--perchfall-color-error: #f87171;
|
|
53
|
+
--perchfall-color-text: #f3f4f6;
|
|
54
|
+
--perchfall-color-muted: #9ca3af;
|
|
55
|
+
--perchfall-color-subtle: #6b7280;
|
|
56
|
+
--perchfall-color-surface: #111827;
|
|
57
|
+
--perchfall-color-surface-alt: #1f2937;
|
|
58
|
+
--perchfall-color-border: #374151;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* -------------------------------------------------------------------
|
|
63
|
+
3. Reset (scoped to .pf-root)
|
|
64
|
+
------------------------------------------------------------------- */
|
|
65
|
+
.pf-root,
|
|
66
|
+
.pf-root *,
|
|
67
|
+
.pf-root *::before,
|
|
68
|
+
.pf-root *::after {
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.pf-root {
|
|
73
|
+
font-family: var(--perchfall-font-family);
|
|
74
|
+
color: var(--perchfall-color-text);
|
|
75
|
+
line-height: var(--perchfall-line-height-normal);
|
|
76
|
+
background: var(--perchfall-color-surface-alt);
|
|
77
|
+
min-height: 100vh;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.pf-root a {
|
|
81
|
+
color: inherit;
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.pf-root hr {
|
|
86
|
+
border: none;
|
|
87
|
+
border-top: 1px solid var(--perchfall-color-border);
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.pf-container {
|
|
92
|
+
max-width: var(--perchfall-container-max);
|
|
93
|
+
margin: 0 auto;
|
|
94
|
+
padding: var(--perchfall-space-8);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* -------------------------------------------------------------------
|
|
98
|
+
4. Utilities (.pf-*)
|
|
99
|
+
------------------------------------------------------------------- */
|
|
100
|
+
|
|
101
|
+
/* Display */
|
|
102
|
+
.pf-flex { display: flex; }
|
|
103
|
+
.pf-grid { display: grid; }
|
|
104
|
+
|
|
105
|
+
/* Flex */
|
|
106
|
+
.pf-flex-col { flex-direction: column; }
|
|
107
|
+
.pf-flex-row { flex-direction: row; }
|
|
108
|
+
.pf-flex-1 { flex: 1 1 0%; }
|
|
109
|
+
.pf-items-start { align-items: flex-start; }
|
|
110
|
+
.pf-items-center { align-items: center; }
|
|
111
|
+
.pf-justify-between { justify-content: space-between; }
|
|
112
|
+
.pf-self-stretch { align-self: stretch; }
|
|
113
|
+
|
|
114
|
+
/* Grid templates */
|
|
115
|
+
.pf-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
116
|
+
.pf-grid-runs { grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr; }
|
|
117
|
+
.pf-grid-summary { grid-template-columns: 1fr 1fr; }
|
|
118
|
+
|
|
119
|
+
/* Gap */
|
|
120
|
+
.pf-gap-0-5 { gap: var(--perchfall-space-0-5); }
|
|
121
|
+
.pf-gap-1 { gap: var(--perchfall-space-1); }
|
|
122
|
+
.pf-gap-2 { gap: var(--perchfall-space-2); }
|
|
123
|
+
.pf-gap-3 { gap: var(--perchfall-space-3); }
|
|
124
|
+
.pf-gap-4 { gap: var(--perchfall-space-4); }
|
|
125
|
+
.pf-gap-6 { gap: var(--perchfall-space-6); }
|
|
126
|
+
|
|
127
|
+
/* Sizing */
|
|
128
|
+
.pf-w-full { width: 100%; }
|
|
129
|
+
.pf-w-64 { width: 16rem; }
|
|
130
|
+
|
|
131
|
+
/* Padding */
|
|
132
|
+
.pf-p-8 { padding: var(--perchfall-space-8); }
|
|
133
|
+
.pf-px-3 { padding-left: var(--perchfall-space-3); padding-right: var(--perchfall-space-3); }
|
|
134
|
+
.pf-px-4 { padding-left: var(--perchfall-space-4); padding-right: var(--perchfall-space-4); }
|
|
135
|
+
.pf-py-2 { padding-top: var(--perchfall-space-2); padding-bottom: var(--perchfall-space-2); }
|
|
136
|
+
|
|
137
|
+
/* Typography: size */
|
|
138
|
+
.pf-text-sm { font-size: var(--perchfall-font-size-sm); }
|
|
139
|
+
.pf-text-base { font-size: var(--perchfall-font-size-base); }
|
|
140
|
+
.pf-text-lg { font-size: var(--perchfall-font-size-lg); }
|
|
141
|
+
.pf-text-xl { font-size: var(--perchfall-font-size-xl); }
|
|
142
|
+
.pf-text-2xl { font-size: var(--perchfall-font-size-2xl); }
|
|
143
|
+
.pf-text-3xl { font-size: var(--perchfall-font-size-3xl); }
|
|
144
|
+
.pf-text-5xl { font-size: var(--perchfall-font-size-5xl); }
|
|
145
|
+
|
|
146
|
+
/* Typography: weight */
|
|
147
|
+
.pf-font-normal { font-weight: 400; }
|
|
148
|
+
.pf-font-semibold { font-weight: 600; }
|
|
149
|
+
.pf-font-extrabold { font-weight: 800; }
|
|
150
|
+
|
|
151
|
+
/* Typography: line-height */
|
|
152
|
+
.pf-leading-tight { line-height: var(--perchfall-line-height-tight); }
|
|
153
|
+
.pf-leading-normal { line-height: var(--perchfall-line-height-normal); }
|
|
154
|
+
|
|
155
|
+
/* Typography: color */
|
|
156
|
+
.pf-text-primary { color: var(--perchfall-color-primary); }
|
|
157
|
+
.pf-text-error { color: var(--perchfall-color-error); }
|
|
158
|
+
.pf-text-muted { color: var(--perchfall-color-muted); }
|
|
159
|
+
.pf-text-subtle { color: var(--perchfall-color-subtle); }
|
|
160
|
+
.pf-text-body { color: var(--perchfall-color-text); }
|
|
161
|
+
|
|
162
|
+
/* Typography: alignment + decoration */
|
|
163
|
+
.pf-text-right { text-align: right; }
|
|
164
|
+
.pf-capitalize { text-transform: capitalize; }
|
|
165
|
+
.pf-underline { text-decoration: underline; }
|
|
166
|
+
.pf-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
167
|
+
.pf-break-all { word-break: break-all; }
|
|
168
|
+
|
|
169
|
+
/* Borders */
|
|
170
|
+
.pf-border { border: 1px solid var(--perchfall-color-border); }
|
|
171
|
+
.pf-border-subtle { border-color: var(--perchfall-color-border); }
|
|
172
|
+
.pf-rounded-lg { border-radius: var(--perchfall-radius-lg); }
|
|
173
|
+
.pf-rounded-2xl { border-radius: var(--perchfall-radius-2xl); }
|
|
174
|
+
|
|
175
|
+
/* Backgrounds */
|
|
176
|
+
.pf-bg-surface { background-color: var(--perchfall-color-surface); }
|
|
177
|
+
.pf-bg-surface-alt { background-color: var(--perchfall-color-surface-alt); }
|
|
178
|
+
|
|
179
|
+
/* Effects */
|
|
180
|
+
.pf-cursor-pointer { cursor: pointer; }
|
|
181
|
+
.pf-opacity-60 { opacity: 0.6; }
|
|
182
|
+
|
|
183
|
+
/* Forms */
|
|
184
|
+
.pf-input,
|
|
185
|
+
.pf-select {
|
|
186
|
+
font: inherit;
|
|
187
|
+
color: var(--perchfall-color-text);
|
|
188
|
+
background-color: var(--perchfall-color-surface);
|
|
189
|
+
border: 1px solid var(--perchfall-color-border);
|
|
190
|
+
border-radius: var(--perchfall-radius-lg);
|
|
191
|
+
padding: var(--perchfall-space-2) var(--perchfall-space-3);
|
|
192
|
+
}
|
|
193
|
+
.pf-input:focus,
|
|
194
|
+
.pf-select:focus {
|
|
195
|
+
outline: 2px solid var(--perchfall-color-primary);
|
|
196
|
+
outline-offset: 1px;
|
|
197
|
+
}
|
|
198
|
+
.pf-button {
|
|
199
|
+
font: inherit;
|
|
200
|
+
color: var(--perchfall-color-primary);
|
|
201
|
+
background-color: var(--perchfall-color-surface);
|
|
202
|
+
border: 1px solid var(--perchfall-color-border);
|
|
203
|
+
border-radius: var(--perchfall-radius-lg);
|
|
204
|
+
padding: var(--perchfall-space-2) var(--perchfall-space-4);
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
}
|
|
208
|
+
.pf-button:hover {
|
|
209
|
+
background-color: var(--perchfall-color-surface-alt);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Hover utilities */
|
|
213
|
+
.pf-hover\:pf-text-primary:hover { color: var(--perchfall-color-primary); }
|
|
214
|
+
|
|
215
|
+
/* Responsive: md (>= 768px) */
|
|
216
|
+
@media (min-width: 768px) {
|
|
217
|
+
.pf-md\:pf-flex-row { flex-direction: row; }
|
|
218
|
+
.pf-md\:pf-items-center { align-items: center; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* -------------------------------------------------------------------
|
|
222
|
+
5. Pagy pagination
|
|
223
|
+
------------------------------------------------------------------- */
|
|
224
|
+
.pf-root nav.pagy {
|
|
225
|
+
display: flex;
|
|
226
|
+
gap: var(--perchfall-space-2);
|
|
227
|
+
align-items: center;
|
|
228
|
+
font-size: var(--perchfall-font-size-sm);
|
|
229
|
+
margin-top: var(--perchfall-space-4);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.pf-root nav.pagy a,
|
|
233
|
+
.pf-root nav.pagy span,
|
|
234
|
+
.pf-root nav.pagy label {
|
|
235
|
+
display: inline-block;
|
|
236
|
+
padding: var(--perchfall-space-2) var(--perchfall-space-3);
|
|
237
|
+
border: 1px solid var(--perchfall-color-border);
|
|
238
|
+
border-radius: var(--perchfall-radius-lg);
|
|
239
|
+
color: var(--perchfall-color-primary);
|
|
240
|
+
background-color: var(--perchfall-color-surface);
|
|
241
|
+
text-decoration: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.pf-root nav.pagy a.current,
|
|
245
|
+
.pf-root nav.pagy .current {
|
|
246
|
+
color: var(--perchfall-color-text);
|
|
247
|
+
background-color: var(--perchfall-color-surface-alt);
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.pf-root nav.pagy a:hover {
|
|
252
|
+
background-color: var(--perchfall-color-surface-alt);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.pf-root nav.pagy .gap {
|
|
256
|
+
border: none;
|
|
257
|
+
background: transparent;
|
|
258
|
+
}
|
|
@@ -3,20 +3,64 @@
|
|
|
3
3
|
module Perchfall
|
|
4
4
|
module Rails
|
|
5
5
|
class SyntheticRunsController < Perchfall::Rails.configuration.base_controller.constantize
|
|
6
|
+
layout "perchfall/rails/application"
|
|
7
|
+
|
|
6
8
|
include ::Pagy::Method
|
|
7
9
|
|
|
8
10
|
def index
|
|
9
|
-
runs =
|
|
11
|
+
runs = resolved_synthetic_runs_scope
|
|
10
12
|
runs = runs.for_status(params[:status]) if SyntheticRun::STATUSES.include?(params[:status])
|
|
11
13
|
runs = runs.for_url(params[:url]) if params[:url].present?
|
|
12
14
|
@pagy, @runs = pagy(:offset, runs)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def show
|
|
16
|
-
@run =
|
|
18
|
+
@run = resolved_synthetic_runs_scope.find(params[:id])
|
|
17
19
|
rescue ActiveRecord::RecordNotFound
|
|
18
20
|
head :not_found
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# ADR 0005's host seam. Hosts define `synthetic_runs_scope` on their
|
|
26
|
+
# `base_controller` to scope the dashboard (e.g. `current_user.synthetic_runs`).
|
|
27
|
+
# This method is the engine's internal dispatcher into that host method
|
|
28
|
+
# — named distinctly from the host hook so:
|
|
29
|
+
#
|
|
30
|
+
# - `respond_to?(:synthetic_runs_scope, true)` consults the host's
|
|
31
|
+
# `respond_to_missing?`, so `method_missing`-based delegation is
|
|
32
|
+
# detected. The previous `defined?(super)` form silently fell through
|
|
33
|
+
# to the engine's unscoped default for that case — an auth-bypass risk.
|
|
34
|
+
# - The host's `synthetic_runs_scope` is reached via normal instance
|
|
35
|
+
# dispatch — no `super` inversion. The previous shape had the engine
|
|
36
|
+
# define a same-named method that called `super` to reach the host's,
|
|
37
|
+
# which is non-obvious to a reader of either side.
|
|
38
|
+
#
|
|
39
|
+
# `respond_to?` still walks the ancestry, so an unrelated method named
|
|
40
|
+
# `synthetic_runs_scope` from a concern or mixin can be picked up —
|
|
41
|
+
# the validation guard below surfaces that as a self-diagnosing error
|
|
42
|
+
# when the unrelated method returns the wrong type.
|
|
43
|
+
#
|
|
44
|
+
# The host's return value is validated; nil or non-relation raises a
|
|
45
|
+
# self-diagnosing error. If the host's relation has no order clause,
|
|
46
|
+
# `recent` is applied — "always ordered" is the seam's contract, not
|
|
47
|
+
# something only `index` patches up.
|
|
48
|
+
def resolved_synthetic_runs_scope
|
|
49
|
+
scope =
|
|
50
|
+
if respond_to?(:synthetic_runs_scope, true)
|
|
51
|
+
synthetic_runs_scope
|
|
52
|
+
else
|
|
53
|
+
Perchfall::Rails::SyntheticRun.recent
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless scope.is_a?(ActiveRecord::Relation)
|
|
57
|
+
raise "synthetic_runs_scope must return an ActiveRecord::Relation " \
|
|
58
|
+
"(got #{scope.class}) — see ADR 0005"
|
|
59
|
+
end
|
|
60
|
+
return scope if scope.order_values.any?
|
|
61
|
+
|
|
62
|
+
scope.recent
|
|
63
|
+
end
|
|
20
64
|
end
|
|
21
65
|
end
|
|
22
66
|
end
|
|
@@ -10,22 +10,12 @@ module Perchfall
|
|
|
10
10
|
def perform(url:, scenario_name: nil, ignore: [], client: nil)
|
|
11
11
|
client ||= Perchfall::Client.new
|
|
12
12
|
report = client.run(url: url, scenario_name: scenario_name, ignore: ignore)
|
|
13
|
-
SyntheticRun::Persist.
|
|
13
|
+
SyntheticRun::Persist.from_report(report, url: url, scenario_name: scenario_name)
|
|
14
14
|
rescue Perchfall::Errors::PageLoadError => e
|
|
15
|
-
SyntheticRun.
|
|
16
|
-
url: url,
|
|
17
|
-
scenario_name: scenario_name,
|
|
18
|
-
status: "error",
|
|
19
|
-
raw_report: e.report.to_h
|
|
20
|
-
)
|
|
15
|
+
SyntheticRun::Persist.from_error(url: url, scenario_name: scenario_name, raw_report: e.report.to_h)
|
|
21
16
|
raise
|
|
22
17
|
rescue Perchfall::Errors::Error => e
|
|
23
|
-
SyntheticRun.
|
|
24
|
-
url: url,
|
|
25
|
-
scenario_name: scenario_name,
|
|
26
|
-
status: "error",
|
|
27
|
-
raw_report: { "error" => e.message }
|
|
28
|
-
)
|
|
18
|
+
SyntheticRun::Persist.from_error(url: url, scenario_name: scenario_name, raw_report: { "error" => e.message })
|
|
29
19
|
raise
|
|
30
20
|
end
|
|
31
21
|
end
|
|
@@ -4,10 +4,46 @@ module Perchfall
|
|
|
4
4
|
module Rails
|
|
5
5
|
class SyntheticRun
|
|
6
6
|
class Persist
|
|
7
|
-
def self.
|
|
8
|
-
new(report, url: url, scenario_name: scenario_name).call
|
|
7
|
+
def self.from_report(report, url:, scenario_name: nil)
|
|
8
|
+
persisting { new(report, url: url, scenario_name: scenario_name).call }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def self.from_error(url:, raw_report:, scenario_name: nil)
|
|
12
|
+
persisting do
|
|
13
|
+
SyntheticRun.create!(
|
|
14
|
+
url: url, scenario_name: scenario_name, status: "error", raw_report: raw_report
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Every entry point that persists a SyntheticRun routes through here so
|
|
20
|
+
# the after_persist hook fires exactly once per persisted run — adding
|
|
21
|
+
# a third entry point can't silently forget the hook, only by-passing
|
|
22
|
+
# this helper can.
|
|
23
|
+
def self.persisting
|
|
24
|
+
run = yield
|
|
25
|
+
invoke_after_persist(run)
|
|
26
|
+
run
|
|
27
|
+
end
|
|
28
|
+
private_class_method :persisting
|
|
29
|
+
|
|
30
|
+
def self.invoke_after_persist(run)
|
|
31
|
+
hook = Perchfall::Rails.configuration.after_persist
|
|
32
|
+
return unless hook
|
|
33
|
+
|
|
34
|
+
hook.call(run)
|
|
35
|
+
rescue StandardError, ScriptError => e
|
|
36
|
+
# Catch ScriptError too — NotImplementedError or LoadError from a host
|
|
37
|
+
# hook would otherwise escape past RunJob's Perchfall-only rescues and
|
|
38
|
+
# supplant the engine's error. Signal/memory exceptions propagate.
|
|
39
|
+
backtrace = (e.backtrace || []).first(5).join("\n ")
|
|
40
|
+
::Rails.logger.error(
|
|
41
|
+
"Perchfall::Rails after_persist hook raised for SyntheticRun ##{run.id}: " \
|
|
42
|
+
"#{e.class}: #{e.message}\n #{backtrace}"
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
private_class_method :invoke_after_persist
|
|
46
|
+
|
|
11
47
|
def initialize(report, url:, scenario_name:)
|
|
12
48
|
@report = report
|
|
13
49
|
@url = url
|
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
|
-
<%= stylesheet_link_tag :
|
|
8
|
+
<%= stylesheet_link_tag "perchfall/rails/application", media: "all" %>
|
|
9
|
+
<% if Perchfall::Rails.configuration.overrides_stylesheet.present? %>
|
|
10
|
+
<%= stylesheet_link_tag Perchfall::Rails.configuration.overrides_stylesheet, media: "all" %>
|
|
11
|
+
<% end %>
|
|
9
12
|
</head>
|
|
10
13
|
<body>
|
|
11
|
-
|
|
14
|
+
<div class="pf-root">
|
|
15
|
+
<div class="pf-container">
|
|
16
|
+
<%= yield %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
12
19
|
</body>
|
|
13
20
|
</html>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<div class="p-8 border border-
|
|
2
|
-
<h3 class="text-3xl font-extrabold leading-
|
|
3
|
-
<%= title %> <span class="text-2xl font-semibold <%= count_color %>">(<%= count %>)</span>
|
|
1
|
+
<div class="pf-p-8 pf-border pf-border-subtle pf-rounded-2xl pf-bg-surface pf-flex pf-flex-col pf-gap-4">
|
|
2
|
+
<h3 class="pf-text-3xl pf-font-extrabold pf-leading-tight <%= heading_color %>">
|
|
3
|
+
<%= title %> <span class="pf-text-2xl pf-font-semibold <%= count_color %>">(<%= count %>)</span>
|
|
4
4
|
</h3>
|
|
5
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-
|
|
8
|
-
<span class="text-sm font-normal text-muted"><%= err["type"] %> — <%= err["location"] %></span>
|
|
6
|
+
<div class="pf-flex pf-flex-col pf-gap-1 <%= 'pf-opacity-60' if dimmed %>">
|
|
7
|
+
<span class="pf-text-xl pf-font-normal pf-text-body pf-break-all"><%= err["text"] %></span>
|
|
8
|
+
<span class="pf-text-sm pf-font-normal pf-text-muted"><%= err["type"] %> — <%= err["location"] %></span>
|
|
9
9
|
</div>
|
|
10
10
|
<% unless i == errors.size - 1 %>
|
|
11
|
-
<hr
|
|
11
|
+
<hr>
|
|
12
12
|
<% end %>
|
|
13
13
|
<% end %>
|
|
14
14
|
</div>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<div class="p-8 border border-
|
|
2
|
-
<h3 class="text-3xl font-extrabold leading-
|
|
3
|
-
<%= title %> <span class="text-2xl font-semibold <%= count_color %>">(<%= count %>)</span>
|
|
1
|
+
<div class="pf-p-8 pf-border pf-border-subtle pf-rounded-2xl pf-bg-surface pf-flex pf-flex-col pf-gap-4">
|
|
2
|
+
<h3 class="pf-text-3xl pf-font-extrabold pf-leading-tight <%= heading_color %>">
|
|
3
|
+
<%= title %> <span class="pf-text-2xl pf-font-semibold <%= count_color %>">(<%= count %>)</span>
|
|
4
4
|
</h3>
|
|
5
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-
|
|
8
|
-
<span class="text-sm font-normal text-muted"><%= err["method"] %> — <%= err["failure"] %></span>
|
|
6
|
+
<div class="pf-flex pf-flex-col pf-gap-1 <%= 'pf-opacity-60' if dimmed %>">
|
|
7
|
+
<span class="pf-text-xl pf-font-normal pf-text-body pf-break-all"><%= err["url"] %></span>
|
|
8
|
+
<span class="pf-text-sm pf-font-normal pf-text-muted"><%= err["method"] %> — <%= err["failure"] %></span>
|
|
9
9
|
</div>
|
|
10
10
|
<% unless i == errors.size - 1 %>
|
|
11
|
-
<hr
|
|
11
|
+
<hr>
|
|
12
12
|
<% end %>
|
|
13
13
|
<% end %>
|
|
14
14
|
</div>
|
|
@@ -1,60 +1,59 @@
|
|
|
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-
|
|
1
|
+
<div class="pf-flex pf-flex-col pf-gap-6 pf-w-full">
|
|
2
|
+
<div class="pf-flex pf-flex-col pf-md:pf-flex-row pf-items-start pf-md:pf-items-center pf-self-stretch pf-justify-between pf-gap-4">
|
|
3
|
+
<h2 class="pf-text-5xl pf-font-extrabold pf-leading-tight pf-text-primary">Synthetic Runs</h2>
|
|
4
4
|
|
|
5
|
-
<%= form_with url: synthetic_runs_path, method: :get, class: "flex flex-row gap-3 items-center" do |f| %>
|
|
5
|
+
<%= form_with url: synthetic_runs_path, method: :get, class: "pf-flex pf-flex-row pf-gap-3 pf-items-center" do |f| %>
|
|
6
6
|
<%= f.select :status,
|
|
7
7
|
Perchfall::Rails::SyntheticRun::STATUSES.map { |status| [status.capitalize, status] },
|
|
8
8
|
{ include_blank: "All statuses", selected: params[:status] },
|
|
9
|
-
class: "
|
|
9
|
+
class: "pf-select pf-text-sm pf-font-semibold pf-text-muted" %>
|
|
10
10
|
<%= f.text_field :url,
|
|
11
11
|
placeholder: "Filter by URL",
|
|
12
12
|
value: params[:url],
|
|
13
|
-
class: "
|
|
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" %>
|
|
13
|
+
class: "pf-input pf-text-sm pf-w-64" %>
|
|
14
|
+
<%= f.submit "Filter", class: "pf-button pf-text-sm" %>
|
|
16
15
|
<% end %>
|
|
17
16
|
</div>
|
|
18
17
|
|
|
19
|
-
<div class="p-8 border border-
|
|
20
|
-
<div class="grid grid-
|
|
21
|
-
<span class="text-lg font-semibold leading-
|
|
22
|
-
<span class="text-lg font-semibold leading-
|
|
23
|
-
<span class="text-lg font-semibold leading-
|
|
24
|
-
<span class="text-lg font-semibold leading-
|
|
25
|
-
<span class="text-lg font-semibold leading-
|
|
26
|
-
<span class="text-lg font-semibold leading-
|
|
18
|
+
<div class="pf-p-8 pf-border pf-border-subtle pf-rounded-2xl pf-bg-surface pf-flex pf-flex-col pf-gap-6">
|
|
19
|
+
<div class="pf-grid pf-grid-runs pf-gap-4 pf-items-center">
|
|
20
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted">URL / Scenario</span>
|
|
21
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted">Status</span>
|
|
22
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted pf-text-right">HTTP</span>
|
|
23
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted pf-text-right">Duration</span>
|
|
24
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted pf-text-right">Net errs</span>
|
|
25
|
+
<span class="pf-text-lg pf-font-semibold pf-leading-normal pf-text-muted pf-text-right">When</span>
|
|
27
26
|
</div>
|
|
28
27
|
|
|
29
|
-
<hr
|
|
28
|
+
<hr>
|
|
30
29
|
|
|
31
30
|
<% if @runs.none? %>
|
|
32
|
-
<p class="text-xl font-normal text-muted">No runs found.</p>
|
|
31
|
+
<p class="pf-text-xl pf-font-normal pf-text-muted">No runs found.</p>
|
|
33
32
|
<% end %>
|
|
34
33
|
|
|
35
34
|
<% @runs.each_with_index do |run, i| %>
|
|
36
|
-
<div class="grid grid-
|
|
37
|
-
<div class="flex flex-col gap-0
|
|
38
|
-
<%= link_to synthetic_run_path(run), class: "text-xl font-normal leading-
|
|
35
|
+
<div class="pf-grid pf-grid-runs pf-gap-4 pf-items-center">
|
|
36
|
+
<div class="pf-flex pf-flex-col pf-gap-0-5">
|
|
37
|
+
<%= link_to synthetic_run_path(run), class: "pf-text-xl pf-font-normal pf-leading-normal pf-text-primary pf-underline pf-truncate" do %>
|
|
39
38
|
<%= run.scenario_name || run.url %>
|
|
40
39
|
<% end %>
|
|
41
40
|
<% if run.scenario_name.present? %>
|
|
42
|
-
<span class="text-sm font-normal text-muted"><%= run.url %></span>
|
|
41
|
+
<span class="pf-text-sm pf-font-normal pf-text-muted"><%= run.url %></span>
|
|
43
42
|
<% end %>
|
|
44
43
|
</div>
|
|
45
44
|
|
|
46
|
-
<span class="<%= run.ok? ? 'text-primary' : 'text-error' %> text-xl font-semibold leading-
|
|
45
|
+
<span class="<%= run.ok? ? 'pf-text-primary' : 'pf-text-error' %> pf-text-xl pf-font-semibold pf-leading-normal">
|
|
47
46
|
<%= run.status.capitalize %>
|
|
48
47
|
</span>
|
|
49
48
|
|
|
50
|
-
<span class="text-xl font-normal leading-
|
|
51
|
-
<span class="text-xl font-normal leading-
|
|
52
|
-
<span class="text-xl font-normal leading-
|
|
53
|
-
<span class="text-xl font-normal leading-
|
|
49
|
+
<span class="pf-text-xl pf-font-normal pf-leading-normal pf-text-body pf-text-right"><%= run.http_status || "—" %></span>
|
|
50
|
+
<span class="pf-text-xl pf-font-normal pf-leading-normal pf-text-body pf-text-right"><%= run.duration_ms ? "#{run.duration_ms}ms" : "—" %></span>
|
|
51
|
+
<span class="pf-text-xl pf-font-normal pf-leading-normal pf-text-body pf-text-right"><%= run.network_error_count %></span>
|
|
52
|
+
<span class="pf-text-xl pf-font-normal pf-leading-normal pf-text-muted pf-text-right"><%= time_ago_in_words(run.created_at) %> ago</span>
|
|
54
53
|
</div>
|
|
55
54
|
|
|
56
55
|
<% unless i == @runs.size - 1 %>
|
|
57
|
-
<hr
|
|
56
|
+
<hr>
|
|
58
57
|
<% end %>
|
|
59
58
|
<% end %>
|
|
60
59
|
</div>
|
|
@@ -1,65 +1,65 @@
|
|
|
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 %>
|
|
1
|
+
<div class="pf-flex pf-flex-col pf-gap-6 pf-w-full">
|
|
2
|
+
<div class="pf-flex pf-flex-row pf-items-center pf-gap-4">
|
|
3
|
+
<%= link_to synthetic_runs_path, class: "pf-text-muted pf-hover:pf-text-primary pf-text-lg pf-w-full" do %>
|
|
4
4
|
← All runs
|
|
5
5
|
<% end %>
|
|
6
6
|
</div>
|
|
7
|
-
<div class="flex flex-row items-center gap-4">
|
|
8
|
-
<h2 class="text-5xl font-extrabold leading-
|
|
7
|
+
<div class="pf-flex pf-flex-row pf-items-center pf-gap-4">
|
|
8
|
+
<h2 class="pf-text-5xl pf-font-extrabold pf-leading-tight pf-text-primary pf-w-full">
|
|
9
9
|
<%= @run.scenario_name.presence || @run.url %>
|
|
10
10
|
</h2>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
<div class="flex flex-col md:flex-row gap-4 self-stretch items-start">
|
|
13
|
+
<div class="pf-flex pf-flex-col pf-md:pf-flex-row pf-gap-4 pf-self-stretch pf-items-start">
|
|
14
14
|
<%# Summary card %>
|
|
15
|
-
<div class="p-8 border border-
|
|
16
|
-
<h3 class="text-3xl font-extrabold leading-
|
|
15
|
+
<div class="pf-p-8 pf-border pf-border-subtle pf-rounded-2xl pf-bg-surface pf-flex pf-flex-col pf-gap-6 pf-flex-1">
|
|
16
|
+
<h3 class="pf-text-3xl pf-font-extrabold pf-leading-tight pf-text-primary">Summary</h3>
|
|
17
17
|
|
|
18
|
-
<dl class="flex flex-col gap-4">
|
|
19
|
-
<div class="grid grid-
|
|
20
|
-
<dt class="text-lg font-semibold text-muted">Status</dt>
|
|
21
|
-
<dd class="text-xl font-semibold <%= @run.ok? ? 'text-primary' : 'text-error' %>">
|
|
18
|
+
<dl class="pf-flex pf-flex-col pf-gap-4">
|
|
19
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
20
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">Status</dt>
|
|
21
|
+
<dd class="pf-text-xl pf-font-semibold <%= @run.ok? ? 'pf-text-primary' : 'pf-text-error' %>">
|
|
22
22
|
<%= @run.status.capitalize %>
|
|
23
23
|
</dd>
|
|
24
24
|
</div>
|
|
25
|
-
<hr
|
|
26
|
-
<div class="grid grid-
|
|
27
|
-
<dt class="text-lg font-semibold text-muted">Scenario</dt>
|
|
28
|
-
<dd class="text-xl font-normal text-
|
|
25
|
+
<hr>
|
|
26
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
27
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">Scenario</dt>
|
|
28
|
+
<dd class="pf-text-xl pf-font-normal pf-text-body pf-break-all"><%= @run.scenario_name %></dd>
|
|
29
29
|
</div>
|
|
30
|
-
<hr
|
|
30
|
+
<hr>
|
|
31
31
|
<% if @run.scenario_name.present? %>
|
|
32
|
-
<div class="grid grid-
|
|
33
|
-
<dt class="text-lg font-semibold text-muted">URL</dt>
|
|
34
|
-
<dd class="text-xl font-normal text-
|
|
32
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
33
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">URL</dt>
|
|
34
|
+
<dd class="pf-text-xl pf-font-normal pf-text-body"><%= @run.url %></dd>
|
|
35
35
|
</div>
|
|
36
|
-
<hr
|
|
36
|
+
<hr>
|
|
37
37
|
<% end %>
|
|
38
|
-
<div class="grid grid-
|
|
39
|
-
<dt class="text-lg font-semibold text-muted">HTTP status</dt>
|
|
40
|
-
<dd class="text-xl font-normal text-
|
|
38
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
39
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">HTTP status</dt>
|
|
40
|
+
<dd class="pf-text-xl pf-font-normal pf-text-body"><%= @run.http_status || "—" %></dd>
|
|
41
41
|
</div>
|
|
42
|
-
<hr
|
|
43
|
-
<div class="grid grid-
|
|
44
|
-
<dt class="text-lg font-semibold text-muted">Duration</dt>
|
|
45
|
-
<dd class="text-xl font-normal text-
|
|
42
|
+
<hr>
|
|
43
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
44
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">Duration</dt>
|
|
45
|
+
<dd class="pf-text-xl pf-font-normal pf-text-body"><%= @run.duration_ms ? "#{@run.duration_ms}ms" : "—" %></dd>
|
|
46
46
|
</div>
|
|
47
|
-
<hr
|
|
48
|
-
<div class="grid grid-
|
|
49
|
-
<dt class="text-lg font-semibold text-muted">Run at</dt>
|
|
50
|
-
<dd class="text-xl font-normal text-
|
|
47
|
+
<hr>
|
|
48
|
+
<div class="pf-grid pf-grid-summary pf-gap-4">
|
|
49
|
+
<dt class="pf-text-lg pf-font-semibold pf-text-muted">Run at</dt>
|
|
50
|
+
<dd class="pf-text-xl pf-font-normal pf-text-body"><%= @run.created_at.strftime("%b %-d, %Y at %H:%M UTC") %></dd>
|
|
51
51
|
</div>
|
|
52
52
|
</dl>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
-
<div class="flex flex-col gap-4 flex-1">
|
|
55
|
+
<div class="pf-flex pf-flex-col pf-gap-4 pf-flex-1">
|
|
56
56
|
<% if @run.network_errors.any? %>
|
|
57
57
|
<%= render "network_error_card",
|
|
58
58
|
errors: @run.network_errors,
|
|
59
59
|
title: "Network Errors",
|
|
60
60
|
count: @run.network_error_count,
|
|
61
|
-
heading_color: "text-primary",
|
|
62
|
-
count_color: "text-error",
|
|
61
|
+
heading_color: "pf-text-primary",
|
|
62
|
+
count_color: "pf-text-error",
|
|
63
63
|
dimmed: false %>
|
|
64
64
|
<% end %>
|
|
65
65
|
|
|
@@ -68,15 +68,15 @@
|
|
|
68
68
|
errors: @run.console_errors,
|
|
69
69
|
title: "Console Errors",
|
|
70
70
|
count: @run.console_error_count,
|
|
71
|
-
heading_color: "text-primary",
|
|
72
|
-
count_color: "text-error",
|
|
71
|
+
heading_color: "pf-text-primary",
|
|
72
|
+
count_color: "pf-text-error",
|
|
73
73
|
dimmed: false %>
|
|
74
74
|
<% end %>
|
|
75
75
|
|
|
76
76
|
<% if @run.network_errors.none? && @run.console_errors.none? %>
|
|
77
|
-
<div class="p-8 border border-
|
|
78
|
-
<h3 class="text-3xl font-extrabold leading-
|
|
79
|
-
<p class="text-xl font-normal text-muted">This run completed cleanly.</p>
|
|
77
|
+
<div class="pf-p-8 pf-border pf-border-subtle pf-rounded-2xl pf-bg-surface pf-flex pf-flex-col pf-gap-2">
|
|
78
|
+
<h3 class="pf-text-3xl pf-font-extrabold pf-leading-tight pf-text-primary">No errors</h3>
|
|
79
|
+
<p class="pf-text-xl pf-font-normal pf-text-muted">This run completed cleanly.</p>
|
|
80
80
|
</div>
|
|
81
81
|
<% end %>
|
|
82
82
|
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
errors: @run.ignored_network_errors,
|
|
86
86
|
title: "Ignored Network Errors",
|
|
87
87
|
count: @run.ignored_network_errors.size,
|
|
88
|
-
heading_color: "text-muted",
|
|
89
|
-
count_color: "text-subtle",
|
|
88
|
+
heading_color: "pf-text-muted",
|
|
89
|
+
count_color: "pf-text-subtle",
|
|
90
90
|
dimmed: true %>
|
|
91
91
|
<% end %>
|
|
92
92
|
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
errors: @run.ignored_console_errors,
|
|
96
96
|
title: "Ignored Console Errors",
|
|
97
97
|
count: @run.ignored_console_errors.size,
|
|
98
|
-
heading_color: "text-muted",
|
|
99
|
-
count_color: "text-subtle",
|
|
98
|
+
heading_color: "pf-text-muted",
|
|
99
|
+
count_color: "pf-text-subtle",
|
|
100
100
|
dimmed: true %>
|
|
101
101
|
<% end %>
|
|
102
102
|
</div>
|
|
@@ -8,4 +8,34 @@ Perchfall::Rails.configure do |config|
|
|
|
8
8
|
|
|
9
9
|
# ActiveJob queue name for synthetic run jobs. Defaults to :synthetic.
|
|
10
10
|
# config.queue_name = :synthetic
|
|
11
|
+
|
|
12
|
+
# Optional host stylesheet (no extension) loaded after the engine CSS.
|
|
13
|
+
# Use to override --perchfall-* CSS custom properties for theming.
|
|
14
|
+
# config.overrides_stylesheet = "perchfall_rails_overrides"
|
|
15
|
+
|
|
16
|
+
# Optional callable invoked after each SyntheticRun is persisted. Receives
|
|
17
|
+
# the persisted run as a single positional argument. Fires on the happy
|
|
18
|
+
# path and on RunJob's error rescues — once per persisted run, which under
|
|
19
|
+
# ActiveJob retries means once per attempt. Use run.id to route downstream
|
|
20
|
+
# work to that specific persisted run (e.g. SomeJob.perform_later(run.id))
|
|
21
|
+
# — each retry attempt persists a distinct id, so run.id does NOT dedup
|
|
22
|
+
# across attempts. The engine provides no cross-attempt key today, so
|
|
23
|
+
# handlers that need once-per-failure-event semantics (notifications,
|
|
24
|
+
# ticket-opens) should be idempotent on the host's natural keys, or
|
|
25
|
+
# should accept at-least-once. Exceptions are rescued and logged; they
|
|
26
|
+
# do not interrupt the pipeline. See ADR 0006.
|
|
27
|
+
# config.after_persist = ->(run) { SomeJob.perform_later(run.id) if run.failed? }
|
|
28
|
+
|
|
29
|
+
# Optional name of a host module the engine includes into
|
|
30
|
+
# Perchfall::Rails::SyntheticRun from a config.to_prepare block on boot
|
|
31
|
+
# (and re-includes on every code reload in development). Use to declare
|
|
32
|
+
# host-side associations, scopes, or callbacks (`belongs_to :user`, &c.).
|
|
33
|
+
# The named module must be autoloadable.
|
|
34
|
+
# Callbacks added here run as part of the engine's SyntheticRun.create!; if
|
|
35
|
+
# one raises, the create fails, the job dies, no SyntheticRun is written
|
|
36
|
+
# (not even an error row), and after_persist does not fire. Reserve extension
|
|
37
|
+
# callbacks for declarations and logic that has been hardened against
|
|
38
|
+
# raising; for observability-style side effects, use after_persist. See
|
|
39
|
+
# ADR 0007.
|
|
40
|
+
# config.synthetic_run_extension = "SyntheticRunExtension"
|
|
11
41
|
end
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module Perchfall
|
|
4
4
|
module Rails
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :base_controller, :queue_name
|
|
6
|
+
attr_accessor :after_persist, :base_controller, :overrides_stylesheet, :queue_name, :synthetic_run_extension
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
|
+
@after_persist = nil
|
|
9
10
|
@base_controller = "ApplicationController"
|
|
11
|
+
@overrides_stylesheet = nil
|
|
10
12
|
@queue_name = :synthetic
|
|
13
|
+
@synthetic_run_extension = nil
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
|
|
@@ -21,6 +21,14 @@ module Perchfall
|
|
|
21
21
|
Perchfall::Rails::RegexpSerializer,
|
|
22
22
|
Perchfall::Rails::IgnoreRuleSerializer
|
|
23
23
|
)
|
|
24
|
+
|
|
25
|
+
# ADR 0007: include the host-provided extension module into SyntheticRun.
|
|
26
|
+
# config.to_prepare fires after all host initializers (so the config is
|
|
27
|
+
# set), after eager_load in production (so the model is loaded), and on
|
|
28
|
+
# every code reload in development (so the include re-applies after
|
|
29
|
+
# Zeitwerk swaps the class). Module#include is idempotent on its own.
|
|
30
|
+
extension = Perchfall::Rails.configuration.synthetic_run_extension
|
|
31
|
+
Perchfall::Rails::SyntheticRun.include(extension.constantize) if extension
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
initializer "perchfall.node_path" do
|
|
@@ -28,6 +36,15 @@ module Perchfall
|
|
|
28
36
|
existing = ENV["NODE_PATH"].to_s
|
|
29
37
|
ENV["NODE_PATH"] = [local_node_modules, existing].reject(&:empty?).join(":")
|
|
30
38
|
end
|
|
39
|
+
|
|
40
|
+
initializer "perchfall.rails.assets" do |app|
|
|
41
|
+
if app.config.respond_to?(:assets)
|
|
42
|
+
app.config.assets.paths << root.join("app/assets/stylesheets").to_s
|
|
43
|
+
if app.config.assets.respond_to?(:precompile)
|
|
44
|
+
app.config.assets.precompile << "perchfall/rails/application.css"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
31
48
|
end
|
|
32
49
|
end
|
|
33
50
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: perchfall-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yossef Mendelssohn
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: pagy
|
|
@@ -55,20 +56,18 @@ description: Mountable Rails engine that provides a background job pipeline, Act
|
|
|
55
56
|
model, controller, and views for running and reviewing Perchfall synthetic browser
|
|
56
57
|
checks.
|
|
57
58
|
email:
|
|
58
|
-
-
|
|
59
|
+
- yossef@beflagrant.com
|
|
59
60
|
executables: []
|
|
60
61
|
extensions: []
|
|
61
62
|
extra_rdoc_files: []
|
|
62
63
|
files:
|
|
63
|
-
- ".nvmrc"
|
|
64
|
-
- ".ruby-version"
|
|
65
64
|
- CHANGELOG.md
|
|
66
65
|
- CODE_OF_CONDUCT.md
|
|
67
66
|
- CONTRIBUTING.md
|
|
68
67
|
- LICENSE.txt
|
|
69
68
|
- README.md
|
|
70
69
|
- Rakefile
|
|
71
|
-
- app/assets/
|
|
70
|
+
- app/assets/stylesheets/perchfall/rails/application.css
|
|
72
71
|
- app/controllers/perchfall/rails/synthetic_runs_controller.rb
|
|
73
72
|
- app/jobs/perchfall/rails/run_job.rb
|
|
74
73
|
- app/models/perchfall/rails/synthetic_run.rb
|
|
@@ -125,7 +124,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
124
|
- !ruby/object:Gem::Version
|
|
126
125
|
version: '0'
|
|
127
126
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
127
|
+
rubygems_version: 3.0.3.1
|
|
128
|
+
signing_key:
|
|
129
129
|
specification_version: 4
|
|
130
130
|
summary: Rails engine for integrating Perchfall synthetic monitoring
|
|
131
131
|
test_files: []
|
data/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
22
|
data/.ruby-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
4.0.2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@source "../../../views/**/*.html.erb";
|