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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62f4ed9c4e72fad6e37235d7831a24fed0f941d5832be21a9469972f04cc40d8
4
- data.tar.gz: dc94f1a8349a01b22819c66602d38a9795670f917e5b55aeba313eddaafea918
3
+ metadata.gz: 50d6da0920fbd92140fcbba4bb15ca4a0b3290827746785c8d5c46e55caaab0c
4
+ data.tar.gz: d9788d25d33b9d74b7bc7ff5e06e15b754391d0ab841de86f870282ca22dd6cb
5
5
  SHA512:
6
- metadata.gz: 2ed06d6d5570260669b7cb7626566aeeaed03f1771a4afc0f0c99377079e68a15f0283068f6b00f749a62a5cf92d16adbaa6849f424f9af22a08ee07f88084ec
7
- data.tar.gz: 01e9d6f47a22a8a60dac0a88efe08835b5be3d0fb0c2fdc28fd0d30c387aaab6fdfa3f3489e20c35ce7b4875c6aed531c314a3771cf4fa731a01d33bb426e717
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.dispose_js
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
@@ -1,5 +1,5 @@
1
1
  module Dommy
2
2
  module Rails
3
- VERSION = "0.9.0"
3
+ VERSION = "0.11.0"
4
4
  end
5
5
  end
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.9.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.9.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.9.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