activesupport 8.0.3 → 8.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +312 -159
  3. data/lib/active_support/backtrace_cleaner.rb +71 -0
  4. data/lib/active_support/cache/mem_cache_store.rb +13 -13
  5. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  6. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  7. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  8. data/lib/active_support/cache.rb +69 -6
  9. data/lib/active_support/callbacks.rb +20 -8
  10. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  11. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  12. data/lib/active_support/configurable.rb +28 -0
  13. data/lib/active_support/continuous_integration.rb +145 -0
  14. data/lib/active_support/core_ext/array.rb +7 -7
  15. data/lib/active_support/core_ext/benchmark.rb +4 -12
  16. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  17. data/lib/active_support/core_ext/class/attribute.rb +8 -6
  18. data/lib/active_support/core_ext/class.rb +2 -2
  19. data/lib/active_support/core_ext/date.rb +5 -5
  20. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  21. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  22. data/lib/active_support/core_ext/date_time.rb +5 -5
  23. data/lib/active_support/core_ext/digest.rb +1 -1
  24. data/lib/active_support/core_ext/enumerable.rb +2 -2
  25. data/lib/active_support/core_ext/erb/util.rb +3 -3
  26. data/lib/active_support/core_ext/file.rb +1 -1
  27. data/lib/active_support/core_ext/hash.rb +8 -8
  28. data/lib/active_support/core_ext/integer.rb +3 -3
  29. data/lib/active_support/core_ext/kernel.rb +3 -3
  30. data/lib/active_support/core_ext/module.rb +11 -11
  31. data/lib/active_support/core_ext/numeric.rb +3 -3
  32. data/lib/active_support/core_ext/object/json.rb +8 -1
  33. data/lib/active_support/core_ext/object/to_query.rb +5 -0
  34. data/lib/active_support/core_ext/object.rb +13 -13
  35. data/lib/active_support/core_ext/pathname.rb +2 -2
  36. data/lib/active_support/core_ext/range.rb +4 -5
  37. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  38. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  39. data/lib/active_support/core_ext/string.rb +13 -13
  40. data/lib/active_support/core_ext/symbol.rb +1 -1
  41. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  42. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  43. data/lib/active_support/core_ext/time.rb +5 -5
  44. data/lib/active_support/core_ext.rb +1 -1
  45. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  46. data/lib/active_support/current_attributes.rb +13 -10
  47. data/lib/active_support/dependencies/interlock.rb +11 -5
  48. data/lib/active_support/dependencies.rb +6 -1
  49. data/lib/active_support/deprecation/reporting.rb +4 -2
  50. data/lib/active_support/deprecation.rb +1 -1
  51. data/lib/active_support/editor.rb +70 -0
  52. data/lib/active_support/error_reporter.rb +50 -6
  53. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  54. data/lib/active_support/event_reporter.rb +592 -0
  55. data/lib/active_support/evented_file_update_checker.rb +5 -1
  56. data/lib/active_support/execution_context.rb +64 -7
  57. data/lib/active_support/file_update_checker.rb +7 -5
  58. data/lib/active_support/gem_version.rb +3 -3
  59. data/lib/active_support/gzip.rb +1 -0
  60. data/lib/active_support/hash_with_indifferent_access.rb +27 -7
  61. data/lib/active_support/i18n_railtie.rb +1 -2
  62. data/lib/active_support/inflector/inflections.rb +31 -15
  63. data/lib/active_support/inflector/transliterate.rb +6 -8
  64. data/lib/active_support/isolated_execution_state.rb +12 -15
  65. data/lib/active_support/json/decoding.rb +2 -2
  66. data/lib/active_support/json/encoding.rb +135 -17
  67. data/lib/active_support/log_subscriber.rb +2 -6
  68. data/lib/active_support/message_encryptors.rb +52 -0
  69. data/lib/active_support/message_pack/extensions.rb +5 -0
  70. data/lib/active_support/message_verifiers.rb +52 -0
  71. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  72. data/lib/active_support/messages/rotator.rb +5 -0
  73. data/lib/active_support/multibyte/chars.rb +8 -1
  74. data/lib/active_support/multibyte.rb +4 -0
  75. data/lib/active_support/notifications/fanout.rb +64 -42
  76. data/lib/active_support/notifications/instrumenter.rb +1 -1
  77. data/lib/active_support/railtie.rb +32 -15
  78. data/lib/active_support/structured_event_subscriber.rb +99 -0
  79. data/lib/active_support/subscriber.rb +0 -5
  80. data/lib/active_support/syntax_error_proxy.rb +3 -0
  81. data/lib/active_support/test_case.rb +61 -6
  82. data/lib/active_support/testing/assertions.rb +34 -6
  83. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  84. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  85. data/lib/active_support/testing/notification_assertions.rb +92 -0
  86. data/lib/active_support/testing/parallelization/server.rb +15 -2
  87. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  88. data/lib/active_support/testing/parallelization.rb +25 -1
  89. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  90. data/lib/active_support/testing/time_helpers.rb +7 -3
  91. data/lib/active_support/time_with_zone.rb +22 -22
  92. data/lib/active_support/values/time_zone.rb +8 -1
  93. data/lib/active_support/xml_mini.rb +3 -2
  94. data/lib/active_support.rb +23 -14
  95. metadata +24 -17
  96. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -67,14 +67,13 @@ module ActiveSupport # :nodoc:
67
67
  original_concat(value)
68
68
  end
69
69
 
70
- def initialize(str = "")
71
- @html_safe = true
70
+ def initialize(_str = "")
72
71
  super
73
72
  end
74
73
 
75
74
  def initialize_copy(other)
76
75
  super
77
- @html_safe = other.html_safe?
76
+ @html_unsafe = true unless other.html_safe?
78
77
  end
79
78
 
80
79
  def concat(value)
@@ -116,7 +115,9 @@ module ActiveSupport # :nodoc:
116
115
  def *(_)
117
116
  new_string = super
118
117
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
119
- new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
118
+ if @html_unsafe
119
+ new_safe_buffer.instance_variable_set(:@html_unsafe, true)
120
+ end
120
121
  new_safe_buffer
121
122
  end
122
123
 
@@ -131,14 +132,18 @@ module ActiveSupport # :nodoc:
131
132
  self.class.new(super(escaped_args))
132
133
  end
133
134
 
134
- attr_reader :html_safe
135
- alias_method :html_safe?, :html_safe
136
- remove_method :html_safe
135
+ def html_safe?
136
+ @html_unsafe.nil?
137
+ end
137
138
 
138
139
  def to_s
139
140
  self
140
141
  end
141
142
 
143
+ def as_json(*)
144
+ to_str
145
+ end
146
+
142
147
  def to_param
143
148
  to_str
144
149
  end
@@ -155,7 +160,7 @@ module ActiveSupport # :nodoc:
155
160
  end # end
156
161
 
157
162
  def #{unsafe_method}!(*args) # def capitalize!(*args)
158
- @html_safe = false # @html_safe = false
163
+ @html_unsafe = true # @html_unsafe = true
159
164
  super # super
160
165
  end # end
161
166
  EOT
@@ -176,7 +181,7 @@ module ActiveSupport # :nodoc:
176
181
  end # end
177
182
 
178
183
  def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
179
- @html_safe = false # @html_safe = false
184
+ @html_unsafe = true # @html_unsafe = true
180
185
  if block # if block
181
186
  super(*args) { |*params| # super(*args) { |*params|
182
187
  set_block_back_references(block, $~) # set_block_back_references(block, $~)
@@ -191,14 +196,14 @@ module ActiveSupport # :nodoc:
191
196
 
192
197
  private
193
198
  def explicit_html_escape_interpolated_argument(arg)
194
- (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
199
+ (!html_safe? || arg.html_safe?) ? arg : ERB::Util.unwrapped_html_escape(arg)
195
200
  end
196
201
 
197
202
  def implicit_html_escape_interpolated_argument(arg)
198
203
  if !html_safe? || arg.html_safe?
199
204
  arg
200
205
  else
201
- CGI.escapeHTML(arg.to_str)
206
+ ERB::Util.unwrapped_html_escape(arg.to_str)
202
207
  end
203
208
  end
204
209
 
@@ -210,7 +215,9 @@ module ActiveSupport # :nodoc:
210
215
 
211
216
  def string_into_safe_buffer(new_string, is_html_safe)
212
217
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
213
- new_safe_buffer.instance_variable_set :@html_safe, is_html_safe
218
+ unless is_html_safe
219
+ new_safe_buffer.instance_variable_set :@html_unsafe, true
220
+ end
214
221
  new_safe_buffer
215
222
  end
216
223
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/string/conversions"
4
- require "active_support/core_ext/string/filters"
5
- require "active_support/core_ext/string/multibyte"
6
- require "active_support/core_ext/string/starts_ends_with"
7
- require "active_support/core_ext/string/inflections"
8
- require "active_support/core_ext/string/access"
9
- require "active_support/core_ext/string/behavior"
10
- require "active_support/core_ext/string/output_safety"
11
- require "active_support/core_ext/string/exclude"
12
- require "active_support/core_ext/string/strip"
13
- require "active_support/core_ext/string/inquiry"
14
- require "active_support/core_ext/string/indent"
15
- require "active_support/core_ext/string/zones"
3
+ require_relative "string/conversions"
4
+ require_relative "string/filters"
5
+ require_relative "string/multibyte"
6
+ require_relative "string/starts_ends_with"
7
+ require_relative "string/inflections"
8
+ require_relative "string/access"
9
+ require_relative "string/behavior"
10
+ require_relative "string/output_safety"
11
+ require_relative "string/exclude"
12
+ require_relative "string/strip"
13
+ require_relative "string/inquiry"
14
+ require_relative "string/indent"
15
+ require_relative "string/zones"
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/symbol/starts_ends_with"
3
+ require_relative "symbol/starts_ends_with"
@@ -224,13 +224,6 @@ class Time
224
224
  # Returns a new Time representing the time a number of seconds since the instance time
225
225
  def since(seconds)
226
226
  self + seconds
227
- rescue TypeError
228
- result = to_datetime.since(seconds)
229
- ActiveSupport.deprecator.warn(
230
- "Passing an instance of #{seconds.class} to #{self.class}#since is deprecated. This behavior will raise " \
231
- "a `TypeError` in Rails 8.1."
232
- )
233
- result
234
227
  end
235
228
  alias :in :since
236
229
 
@@ -8,33 +8,8 @@ class Time
8
8
 
9
9
  silence_redefinition_of_method :to_time
10
10
 
11
- # Either return +self+ or the time in the local system timezone depending
12
- # on the setting of +ActiveSupport.to_time_preserves_timezone+.
11
+ # Return +self+.
13
12
  def to_time
14
- preserve_timezone ? self : getlocal
13
+ self
15
14
  end
16
-
17
- def preserve_timezone # :nodoc:
18
- system_local_time? || super
19
- end
20
-
21
- private
22
- def system_local_time?
23
- if ::Time.equal?(self.class)
24
- zone = self.zone
25
- String === zone &&
26
- (zone != "UTC" || active_support_local_zone == "UTC")
27
- end
28
- end
29
-
30
- @@active_support_local_tz = nil
31
-
32
- def active_support_local_zone
33
- @@active_support_local_zone = nil if @@active_support_local_tz != ENV["TZ"]
34
- @@active_support_local_zone ||=
35
- begin
36
- @@active_support_local_tz = ENV["TZ"]
37
- Time.new.zone
38
- end
39
- end
40
15
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/time/acts_like"
4
- require "active_support/core_ext/time/calculations"
5
- require "active_support/core_ext/time/compatibility"
6
- require "active_support/core_ext/time/conversions"
7
- require "active_support/core_ext/time/zones"
3
+ require_relative "time/acts_like"
4
+ require_relative "time/calculations"
5
+ require_relative "time/compatibility"
6
+ require_relative "time/conversions"
7
+ require_relative "time/zones"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
3
+ (Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort - [File.expand_path("core_ext/benchmark.rb", __dir__)]).each do |path|
4
4
  require path
5
5
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
4
4
  def before_setup
5
- ActiveSupport::CurrentAttributes.reset_all
5
+ ActiveSupport::CurrentAttributes.clear_all
6
6
  super
7
7
  end
8
8
 
9
9
  def after_teardown
10
10
  super
11
- ActiveSupport::CurrentAttributes.reset_all
11
+ ActiveSupport::CurrentAttributes.clear_all
12
12
  end
13
13
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/callbacks"
4
+ require "active_support/execution_context"
4
5
  require "active_support/core_ext/object/with"
5
6
  require "active_support/core_ext/enumerable"
6
7
  require "active_support/core_ext/module/delegation"
@@ -125,13 +126,13 @@ module ActiveSupport
125
126
  owner.define_cached_method(name, namespace: :current_attributes) do |batch|
126
127
  batch <<
127
128
  "def #{name}" <<
128
- "attributes[:#{name}]" <<
129
+ "@attributes[:#{name}]" <<
129
130
  "end"
130
131
  end
131
132
  owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
132
133
  batch <<
133
134
  "def #{name}=(value)" <<
134
- "attributes[:#{name}] = value" <<
135
+ "@attributes[:#{name}] = value" <<
135
136
  "end"
136
137
  end
137
138
  end
@@ -153,13 +154,11 @@ module ActiveSupport
153
154
 
154
155
  delegate :set, :reset, to: :instance
155
156
 
156
- def reset_all # :nodoc:
157
- current_instances.each_value(&:reset)
158
- end
159
-
160
157
  def clear_all # :nodoc:
161
- reset_all
162
- current_instances.clear
158
+ if instances = current_instances
159
+ instances.values.each(&:reset)
160
+ instances.clear
161
+ end
163
162
  end
164
163
 
165
164
  private
@@ -168,7 +167,7 @@ module ActiveSupport
168
167
  end
169
168
 
170
169
  def current_instances
171
- IsolatedExecutionState[:current_attributes_instances] ||= {}
170
+ ExecutionContext.current_attributes_instances
172
171
  end
173
172
 
174
173
  def current_instances_key
@@ -201,12 +200,16 @@ module ActiveSupport
201
200
 
202
201
  class_attribute :defaults, instance_writer: false, default: {}.freeze
203
202
 
204
- attr_accessor :attributes
203
+ attr_writer :attributes
205
204
 
206
205
  def initialize
207
206
  @attributes = resolve_defaults
208
207
  end
209
208
 
209
+ def attributes
210
+ @attributes.dup
211
+ end
212
+
210
213
  # Expose one or more attributes within a block. Old values are returned after the block concludes.
211
214
  # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
212
215
  #
@@ -10,19 +10,24 @@ module ActiveSupport # :nodoc:
10
10
  end
11
11
 
12
12
  def loading(&block)
13
- @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load], &block)
13
+ ActiveSupport.deprecator.warn(
14
+ "ActiveSupport::Dependencies::Interlock#loading is deprecated and " \
15
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
16
+ "used since Rails switched to Zeitwerk for autoloading."
17
+ )
18
+ yield if block
14
19
  end
15
20
 
16
21
  def unloading(&block)
17
- @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], &block)
22
+ @lock.exclusive(purpose: :unload, compatible: [:unload], after_compatible: [:unload], &block)
18
23
  end
19
24
 
20
25
  def start_unloading
21
- @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
26
+ @lock.start_exclusive(purpose: :unload, compatible: [:unload])
22
27
  end
23
28
 
24
29
  def done_unloading
25
- @lock.stop_exclusive(compatible: [:load, :unload])
30
+ @lock.stop_exclusive(compatible: [:unload])
26
31
  end
27
32
 
28
33
  def start_running
@@ -38,7 +43,8 @@ module ActiveSupport # :nodoc:
38
43
  end
39
44
 
40
45
  def permit_concurrent_loads(&block)
41
- @lock.yield_shares(compatible: [:load], &block)
46
+ # Soft deprecated: no deprecation warning for now, but this is a no-op.
47
+ yield if block
42
48
  end
43
49
 
44
50
  def raw_state(&block) # :nodoc:
@@ -21,7 +21,12 @@ module ActiveSupport # :nodoc:
21
21
  # preventing any other thread from being inside a #run_interlock
22
22
  # block at the same time.
23
23
  def self.load_interlock(&block)
24
- interlock.loading(&block)
24
+ ActiveSupport.deprecator.warn(
25
+ "ActiveSupport::Dependencies.load_interlock is deprecated and " \
26
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
27
+ "used since Rails switched to Zeitwerk for autoloading."
28
+ )
29
+ yield if block
25
30
  end
26
31
 
27
32
  # Execute the supplied block while holding an exclusive lock,
@@ -149,8 +149,10 @@ module ActiveSupport
149
149
  [offending_line.path, offending_line.lineno, offending_line.label]
150
150
  end
151
151
 
152
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
153
- LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
152
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
153
+ private_constant :RAILS_GEM_ROOT
154
+ LIB_DIR = RbConfig::CONFIG["libdir"]
155
+ private_constant :LIB_DIR
154
156
 
155
157
  def ignored_callstack?(path)
156
158
  path.start_with?(RAILS_GEM_ROOT, LIB_DIR) || path.include?("<internal:")
@@ -68,7 +68,7 @@ module ActiveSupport
68
68
  # and the second is a library name.
69
69
  #
70
70
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
71
- def initialize(deprecation_horizon = "8.1", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.2", gem_name = "Rails")
72
72
  self.gem_name = gem_name
73
73
  self.deprecation_horizon = deprecation_horizon
74
74
  # By default, warnings are not silenced and debugging is off.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActiveSupport
6
+ class Editor
7
+ @editors = {}
8
+ @current = false
9
+
10
+ class << self
11
+ # Registers a URL pattern for opening file in a given editor.
12
+ # This allows Rails to generate clickable links to control known editors.
13
+ #
14
+ # Example:
15
+ #
16
+ # ActiveSupport::Editor.register("myeditor", "myeditor://%s:%d")
17
+ def register(name, url_pattern, aliases: [])
18
+ editor = new(url_pattern)
19
+ @editors[name] = editor
20
+ aliases.each do |a|
21
+ @editors[a] = editor
22
+ end
23
+ end
24
+
25
+ # Returns the current editor pattern if it is known.
26
+ # First check for the `RAILS_EDITOR` environment variable, and if it's
27
+ # missing, check for the `EDITOR` environment variable.
28
+ def current
29
+ if @current == false
30
+ @current = if editor_name = ENV["RAILS_EDITOR"] || ENV["EDITOR"]
31
+ @editors[editor_name]
32
+ end
33
+ end
34
+ @current
35
+ end
36
+
37
+ # :nodoc:
38
+
39
+ def find(name)
40
+ @editors[name]
41
+ end
42
+
43
+ def reset
44
+ @current = false
45
+ end
46
+ end
47
+
48
+ def initialize(url_pattern)
49
+ @url_pattern = url_pattern
50
+ end
51
+
52
+ def url_for(path, line)
53
+ sprintf(@url_pattern, path, line)
54
+ end
55
+
56
+ register "atom", "atom://core/open/file?filename=%s&line=%d"
57
+ register "cursor", "cursor://file/%s:%f"
58
+ register "emacs", "emacs://open?url=file://%s&line=%d", aliases: ["emacsclient"]
59
+ register "idea", "idea://open?file=%s&line=%d"
60
+ register "macvim", "mvim://open?url=file://%s&line=%d", aliases: ["mvim"]
61
+ register "nova", "nova://open?path=%s&line=%d"
62
+ register "rubymine", "x-mine://open?file=%s&line=%d"
63
+ register "sublime", "subl://open?url=file://%s&line=%d", aliases: ["subl"]
64
+ register "textmate", "txmt://open?url=file://%s&line=%d", aliases: ["mate"]
65
+ register "vscode", "vscode://file/%s:%d", aliases: ["code"]
66
+ register "vscodium", "vscodium://file/%s:%d", aliases: ["codium"]
67
+ register "windsurf", "windsurf://file/%s:%d"
68
+ register "zed", "zed://file/%s:%d"
69
+ end
70
+ end
@@ -36,6 +36,7 @@ module ActiveSupport
36
36
  @subscribers = subscribers.flatten
37
37
  @logger = logger
38
38
  @debug_mode = false
39
+ @context_middlewares = ErrorContextMiddlewareStack.new
39
40
  end
40
41
 
41
42
  # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -202,6 +203,22 @@ module ActiveSupport
202
203
  ActiveSupport::ExecutionContext.set(...)
203
204
  end
204
205
 
206
+ # Add a middleware to modify the error context before it is sent to subscribers.
207
+ #
208
+ # Middleware is added to a stack of callables run on an error's execution context
209
+ # before passing to subscribers. Allows creation of entries in error context that
210
+ # are shared by all subscribers.
211
+ #
212
+ # A context middleware receives the same parameters as #report.
213
+ # It must return a hash - the middleware stack returns the hash after it has
214
+ # run through all middlewares. A middleware can mutate or replace the hash.
215
+ #
216
+ # Rails.error.add_middleware(-> (error, context) { context.merge({ foo: :bar }) })
217
+ #
218
+ def add_middleware(middleware)
219
+ @context_middlewares.use(middleware)
220
+ end
221
+
205
222
  # Report an error directly to subscribers. You can use this method when the
206
223
  # block-based #handle and #record methods are not suitable.
207
224
  #
@@ -215,13 +232,22 @@ module ActiveSupport
215
232
  # string argument.
216
233
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
217
234
  return if error.instance_variable_defined?(:@__rails_error_reported)
235
+ raise ArgumentError, "Reported error must be an Exception, got: #{error.inspect}" unless error.is_a?(Exception)
236
+
218
237
  ensure_backtrace(error)
219
238
 
220
239
  unless SEVERITIES.include?(severity)
221
240
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
222
241
  end
223
242
 
224
- full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
243
+ full_context = @context_middlewares.execute(
244
+ error,
245
+ context: ActiveSupport::ExecutionContext.to_h.merge(context || {}),
246
+ handled:,
247
+ severity:,
248
+ source:
249
+ )
250
+
225
251
  disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
226
252
  @subscribers.each do |subscriber|
227
253
  unless disabled_subscribers&.any? { |s| s === subscriber }
@@ -250,14 +276,12 @@ module ActiveSupport
250
276
 
251
277
  private
252
278
  def ensure_backtrace(error)
253
- return if error.frozen? # re-raising won't add a backtrace
279
+ return if error.frozen? # re-raising won't add a backtrace or set the cause
254
280
  return unless error.backtrace.nil?
255
281
 
256
282
  begin
257
- # We could use Exception#set_backtrace, but until Ruby 3.4
258
- # it only support setting `Exception#backtrace` and not
259
- # `Exception#backtrace_locations`. So raising the exception
260
- # is a good way to build a real backtrace.
283
+ # As of Ruby 3.4, we could use Exception#set_backtrace to set the backtrace,
284
+ # but there's nothing like Exception#set_cause. Raising+rescuing is the only way to set the cause.
261
285
  raise error
262
286
  rescue error.class => error
263
287
  end
@@ -270,5 +294,25 @@ module ActiveSupport
270
294
 
271
295
  error.backtrace.shift(count)
272
296
  end
297
+
298
+ class ErrorContextMiddlewareStack # :nodoc:
299
+ def initialize
300
+ @stack = []
301
+ end
302
+
303
+ # Add a middleware to the error context stack.
304
+ def use(middleware)
305
+ unless middleware.respond_to?(:call)
306
+ raise ArgumentError, "Error context middleware must respond to #call"
307
+ end
308
+
309
+ @stack << middleware
310
+ end
311
+
312
+ # Run all middlewares in the stack
313
+ def execute(error, handled:, severity:, context:, source:)
314
+ @stack.inject(context) { |c, middleware| middleware.call(error, context: c, handled:, severity:, source:) }
315
+ end
316
+ end
273
317
  end
274
318
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::EventReporter::TestHelper # :nodoc:
4
+ class EventSubscriber # :nodoc:
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def emit(event)
12
+ @events << event
13
+ end
14
+ end
15
+
16
+ def event_matcher(name:, payload: nil, tags: {}, context: {}, source_location: nil)
17
+ ->(event) {
18
+ return false unless event[:name] == name
19
+ return false unless event[:payload] == payload
20
+ return false unless event[:tags] == tags
21
+ return false unless event[:context] == context
22
+
23
+ [:filepath, :lineno, :label].each do |key|
24
+ if source_location && source_location[key]
25
+ return false unless event[:source_location][key] == source_location[key]
26
+ end
27
+ end
28
+
29
+ true
30
+ }
31
+ end
32
+ end