dommy-rails 0.9.0 → 0.11.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/README.md +29 -0
- data/lib/dommy/rails/browser_spec.rb +17 -5
- data/lib/dommy/rails/trace_instrumentation.rb +140 -0
- data/lib/dommy/rails/version.rb +1 -1
- data/lib/dommy/rails.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50d6da0920fbd92140fcbba4bb15ca4a0b3290827746785c8d5c46e55caaab0c
|
|
4
|
+
data.tar.gz: d9788d25d33b9d74b7bc7ff5e06e15b754391d0ab841de86f870282ca22dd6cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbe6783f0111e3034bd2c26adb2a947f0b43337db68911679af0cdc7892a7a387a082f300c36fca0448b773b0b4cb509a77611fde462249207616c3d86985966
|
|
7
|
+
data.tar.gz: f52246bc241b4a4e67e1fc3ac0fa3cc194bf42895c484291e38f98aedb00fe17cc653aee5482b0470622b357102d9b7facc08de1b59414f75a5499a839756624
|
data/README.md
CHANGED
|
@@ -108,6 +108,35 @@ expect(mail).to have_html_text("Confirm your account")
|
|
|
108
108
|
expect(mail).to have_plain_text("Welcome")
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Failure traces in CI
|
|
112
|
+
|
|
113
|
+
A failed browser test saves a self-contained bundle under
|
|
114
|
+
`tmp/dommy/failures/<example-slug>/` — the page HTML, the readable trace,
|
|
115
|
+
and a machine-readable `trace.ndjson` (with Rails-internals spans:
|
|
116
|
+
controller, SQL, renders, jobs, mail) that the standalone `dommylizer`
|
|
117
|
+
viewer opens. The failure output prints the exact path and command.
|
|
118
|
+
|
|
119
|
+
To keep them from CI runs, upload the directory as an artifact:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
# GitHub Actions
|
|
123
|
+
- uses: actions/upload-artifact@v4
|
|
124
|
+
if: failure()
|
|
125
|
+
with:
|
|
126
|
+
name: dommy-traces
|
|
127
|
+
path: tmp/dommy/failures/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then download the artifact locally and run
|
|
131
|
+
`dommylizer tmp/dommy/failures/<example>/trace.ndjson`.
|
|
132
|
+
|
|
133
|
+
SQL bind values are excluded from traces by default. To include them
|
|
134
|
+
(masked through the same sensitive-key filter as form params):
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
Dommy::Rails::TraceInstrumentation.install!(binds: true)
|
|
138
|
+
```
|
|
139
|
+
|
|
111
140
|
## URL normalization
|
|
112
141
|
|
|
113
142
|
`have_link(href:)`, `have_form(action:)`, and their Minitest counterparts
|
|
@@ -25,8 +25,14 @@ module Dommy
|
|
|
25
25
|
def self.included(base)
|
|
26
26
|
if base.respond_to?(:after)
|
|
27
27
|
base.after do |example|
|
|
28
|
-
dommy_browser_after(failed: example.exception ? true : false,
|
|
28
|
+
dir = dommy_browser_after(failed: example.exception ? true : false,
|
|
29
29
|
label: example.full_description, exception: example.exception)
|
|
30
|
+
# Point the failure output at the saved bundle and the one command
|
|
31
|
+
# that opens it in the standalone viewer.
|
|
32
|
+
if dir && example.respond_to?(:metadata)
|
|
33
|
+
(example.metadata[:extra_failure_lines] ||= []) <<
|
|
34
|
+
"Trace bundle: #{dir}" << "View it with: dommylizer #{::File.join(dir, "trace.ndjson")}"
|
|
35
|
+
end
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
end
|
|
@@ -34,18 +40,21 @@ module Dommy
|
|
|
34
40
|
# Minitest teardown hook (no-op outside Minitest).
|
|
35
41
|
def after_teardown
|
|
36
42
|
failures = respond_to?(:failures) ? self.failures : []
|
|
37
|
-
dommy_browser_after(failed: !failures.empty?, label: (name if respond_to?(:name)),
|
|
43
|
+
dir = dommy_browser_after(failed: !failures.empty?, label: (name if respond_to?(:name)),
|
|
38
44
|
exception: failures.first)
|
|
45
|
+
warn "Trace bundle: #{dir}\nView it with: dommylizer #{::File.join(dir, "trace.ndjson")}" if dir
|
|
39
46
|
ensure
|
|
40
47
|
super if defined?(super)
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
# On a failed example, write debugging artifacts (page HTML + trace +
|
|
44
51
|
# visible text) before disposing, then run the normal teardown. Shared by
|
|
45
|
-
# the RSpec and Minitest hooks.
|
|
52
|
+
# the RSpec and Minitest hooks. Returns the artifacts directory (nil when
|
|
53
|
+
# nothing was saved).
|
|
46
54
|
def dommy_browser_after(failed:, label: nil, exception: nil)
|
|
47
|
-
dommy_save_failure_artifacts(label, exception: exception) if failed && browser_started?
|
|
55
|
+
dir = (dommy_save_failure_artifacts(label, exception: exception) if failed && browser_started?)
|
|
48
56
|
dommy_browser_teardown
|
|
57
|
+
dir
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
# The Rack app the browser drives. Defaults to the Rails application;
|
|
@@ -63,6 +72,9 @@ module Dommy
|
|
|
63
72
|
def browser
|
|
64
73
|
@dommy_browser ||= begin
|
|
65
74
|
require "dommy/js/quickjs/rack"
|
|
75
|
+
# Rails-internals spans (controller/SQL/render/job/mail) on every
|
|
76
|
+
# traced request; idempotent, no-op without ActiveSupport.
|
|
77
|
+
Dommy::Rails::TraceInstrumentation.install!
|
|
66
78
|
::Dommy::Rack::Session.new(dommy_browser_app, javascript: true, trace: true, trace_dom: true,
|
|
67
79
|
trace_snapshots: true)
|
|
68
80
|
end
|
|
@@ -89,7 +101,7 @@ module Dommy
|
|
|
89
101
|
return unless browser_started?
|
|
90
102
|
|
|
91
103
|
pending = browser.js_errors[(@dommy_browser_acked || 0)..] || []
|
|
92
|
-
browser.
|
|
104
|
+
browser.dispose
|
|
93
105
|
@dommy_browser = nil
|
|
94
106
|
return if @dommy_allow_js_errors || pending.empty?
|
|
95
107
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Rails
|
|
5
|
+
# Fills the inside of a traced request: subscribes to the Rails
|
|
6
|
+
# ActiveSupport::Notifications topics (controller action, SQL, template /
|
|
7
|
+
# partial rendering, job enqueue/perform, mail delivery) and buffers each
|
|
8
|
+
# finished event as a completed span on the trace whose request is in
|
|
9
|
+
# flight — Dommy calls the app synchronously, so
|
|
10
|
+
# `Thread.current[:__dommy_active_trace__]` (set by Dommy::Rack::Trace
|
|
11
|
+
# around the Rack call) IS the request's trace.
|
|
12
|
+
#
|
|
13
|
+
# Installed automatically when a BrowserSpec test boots its browser;
|
|
14
|
+
# callable directly (idempotent) from a suite hook too:
|
|
15
|
+
#
|
|
16
|
+
# Dommy::Rails::TraceInstrumentation.install!
|
|
17
|
+
# Dommy::Rails::TraceInstrumentation.install!(binds: true)
|
|
18
|
+
#
|
|
19
|
+
# SQL bind values are excluded by default; `binds: true` includes them as
|
|
20
|
+
# {name => value}, masked through the same sensitive-key filter the trace
|
|
21
|
+
# applies to form params (password/token/… become [FILTERED]).
|
|
22
|
+
module TraceInstrumentation
|
|
23
|
+
TOPICS = {
|
|
24
|
+
"process_action.action_controller" => :controller,
|
|
25
|
+
"sql.active_record" => :db,
|
|
26
|
+
"render_template.action_view" => :render,
|
|
27
|
+
"render_partial.action_view" => :render,
|
|
28
|
+
"enqueue.active_job" => :job,
|
|
29
|
+
"perform.active_job" => :job,
|
|
30
|
+
"deliver.action_mailer" => :mail,
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Statement names carrying no application information.
|
|
34
|
+
SKIPPED_SQL_NAMES = ["SCHEMA", "TRANSACTION"].freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# `binds:` is applied on EVERY call, including calls that find the
|
|
39
|
+
# subscriptions already installed, so a suite hook can turn SQL bind
|
|
40
|
+
# recording on — and back off — without a way to un-subscribe.
|
|
41
|
+
def install!(binds: false)
|
|
42
|
+
@include_binds = binds
|
|
43
|
+
return false if @installed
|
|
44
|
+
return false unless defined?(::ActiveSupport::Notifications)
|
|
45
|
+
return false unless ::ActiveSupport::Notifications.respond_to?(:monotonic_subscribe)
|
|
46
|
+
|
|
47
|
+
TOPICS.each do |topic, kind|
|
|
48
|
+
::ActiveSupport::Notifications.monotonic_subscribe(topic) do |_name, started, finished, _id, payload|
|
|
49
|
+
record(kind, topic, (finished - started) * 1000.0, payload)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
@installed = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def record(kind, topic, duration_ms, payload)
|
|
56
|
+
trace = Thread.current[active_trace_key]
|
|
57
|
+
return unless trace.respond_to?(:__internal_record_span__)
|
|
58
|
+
|
|
59
|
+
span = build_span(kind, topic, payload)
|
|
60
|
+
return unless span
|
|
61
|
+
|
|
62
|
+
trace.__internal_record_span__(kind: kind, label: span[:label],
|
|
63
|
+
duration_ms: duration_ms, data: span[:data])
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil # instrumentation must never break the request
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_span(kind, topic, payload)
|
|
69
|
+
case kind
|
|
70
|
+
when :controller
|
|
71
|
+
{label: "#{payload[:controller]}##{payload[:action]}",
|
|
72
|
+
data: {format: payload[:format].to_s, status: payload[:status]}.compact}
|
|
73
|
+
when :db then db_span(payload)
|
|
74
|
+
when :render
|
|
75
|
+
identifier = payload[:identifier].to_s
|
|
76
|
+
# The app-relative template path reads better than the absolute one.
|
|
77
|
+
label = identifier.sub(%r{\A.*/app/views/}, "")
|
|
78
|
+
{label: "#{topic.start_with?("render_partial") ? "partial" : "template"} #{label}",
|
|
79
|
+
data: nil}
|
|
80
|
+
when :job
|
|
81
|
+
job = payload[:job]
|
|
82
|
+
{label: "#{topic.start_with?("enqueue") ? "enqueue" : "perform"} #{job.class.name}",
|
|
83
|
+
data: ({queue: job.queue_name.to_s} if job.respond_to?(:queue_name))}
|
|
84
|
+
when :mail
|
|
85
|
+
{label: payload[:mailer].to_s, data: nil}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def db_span(payload)
|
|
90
|
+
name = payload[:name].to_s
|
|
91
|
+
return nil if SKIPPED_SQL_NAMES.include?(name)
|
|
92
|
+
|
|
93
|
+
data = {sql: payload[:sql].to_s}
|
|
94
|
+
if @include_binds && (masked = masked_binds(payload))
|
|
95
|
+
data[:binds] = masked
|
|
96
|
+
end
|
|
97
|
+
{label: name.empty? ? "SQL" : name, data: data}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# {attribute name => type-cast value}, sensitive keys masked with the
|
|
101
|
+
# in-flight trace's OWN filter (so custom filter keys apply to binds
|
|
102
|
+
# exactly as to form params), falling back to the shared default. nil
|
|
103
|
+
# when the payload carries no usable binds.
|
|
104
|
+
def masked_binds(payload)
|
|
105
|
+
names = Array(payload[:binds]).map { |b| b.respond_to?(:name) ? b.name.to_s : b.to_s }
|
|
106
|
+
values = Array(type_casted_binds(payload[:type_casted_binds]))
|
|
107
|
+
return nil if names.empty? || names.length != values.length
|
|
108
|
+
|
|
109
|
+
bind_filter.form_params(names.zip(values))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Some adapters/versions publish `type_casted_binds` as a callable that
|
|
113
|
+
# defers the (potentially expensive) casting until a subscriber asks for
|
|
114
|
+
# it — exactly what ActiveRecord::LogSubscriber unwraps before reading it.
|
|
115
|
+
# Without this, a single-bind statement would record the Proc ITSELF as
|
|
116
|
+
# the bind value, and every other statement would silently drop its binds
|
|
117
|
+
# on the names/values length check.
|
|
118
|
+
def type_casted_binds(casted_binds)
|
|
119
|
+
casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bind_filter
|
|
123
|
+
trace = Thread.current[active_trace_key]
|
|
124
|
+
return trace.__internal_param_filter__ if trace.respond_to?(:__internal_param_filter__)
|
|
125
|
+
|
|
126
|
+
@default_filter ||= Dommy::Rack::Trace::ParamFilter.new(Dommy::Rack::Trace::ParamFilter::DEFAULT)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The Trace owns the thread-local's name; read it from there when
|
|
130
|
+
# dommy-rack is loaded so the two can never drift apart.
|
|
131
|
+
def active_trace_key
|
|
132
|
+
if defined?(::Dommy::Rack::Trace::TRACE_THREAD_KEY)
|
|
133
|
+
::Dommy::Rack::Trace::TRACE_THREAD_KEY
|
|
134
|
+
else
|
|
135
|
+
:__dommy_active_trace__
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/dommy/rails/version.rb
CHANGED
data/lib/dommy/rails.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "rails/mail_part"
|
|
|
10
10
|
require_relative "rails/match_target"
|
|
11
11
|
require_relative "rails/dom_source"
|
|
12
12
|
require_relative "rails/page_inspector"
|
|
13
|
+
require_relative "rails/trace_instrumentation"
|
|
13
14
|
require_relative "rails/aria_snapshot_matching"
|
|
14
15
|
|
|
15
16
|
module Dommy
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dommy-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.11.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.11.0
|
|
26
26
|
description: Rails-specific matchers and assertions for Dommy, including form helper
|
|
27
27
|
understanding, Turbo Stream support, Stimulus attribute checking, and HTML quality
|
|
28
28
|
linting.
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/dommy/rails/rspec/integration.rb
|
|
51
51
|
- lib/dommy/rails/rspec/matchers.rb
|
|
52
52
|
- lib/dommy/rails/stimulus.rb
|
|
53
|
+
- lib/dommy/rails/trace_instrumentation.rb
|
|
53
54
|
- lib/dommy/rails/turbo_stream.rb
|
|
54
55
|
- lib/dommy/rails/url_matcher.rb
|
|
55
56
|
- lib/dommy/rails/url_normalizer.rb
|