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
@@ -0,0 +1,592 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/parameter_filter"
4
+
5
+ module ActiveSupport
6
+ class TagStack # :nodoc:
7
+ EMPTY_TAGS = {}.freeze
8
+ FIBER_KEY = :event_reporter_tags
9
+
10
+ class << self
11
+ def tags
12
+ Fiber[FIBER_KEY] || EMPTY_TAGS
13
+ end
14
+
15
+ def with_tags(*args, **kwargs)
16
+ existing_tags = tags
17
+ tags = existing_tags.dup
18
+ tags.merge!(resolve_tags(args, kwargs))
19
+ new_tags = tags.freeze
20
+
21
+ begin
22
+ Fiber[FIBER_KEY] = new_tags
23
+ yield
24
+ ensure
25
+ Fiber[FIBER_KEY] = existing_tags
26
+ end
27
+ end
28
+
29
+ private
30
+ def resolve_tags(args, kwargs)
31
+ tags = args.each_with_object({}) do |arg, tags|
32
+ case arg
33
+ when String
34
+ tags[arg.to_sym] = true
35
+ when Symbol
36
+ tags[arg] = true
37
+ when Hash
38
+ arg.each { |key, value| tags[key.to_sym] = value }
39
+ else
40
+ tags[arg.class.name.to_sym] = arg
41
+ end
42
+ end
43
+ kwargs.each { |key, value| tags[key.to_sym] = value }
44
+ tags
45
+ end
46
+ end
47
+ end
48
+
49
+ class EventContext # :nodoc:
50
+ EMPTY_CONTEXT = {}.freeze
51
+ FIBER_KEY = :event_reporter_context
52
+
53
+ class << self
54
+ def context
55
+ Fiber[FIBER_KEY] || EMPTY_CONTEXT
56
+ end
57
+
58
+ def set_context(context_hash)
59
+ new_context = self.context.dup
60
+ context_hash.each { |key, value| new_context[key.to_sym] = value }
61
+
62
+ Fiber[FIBER_KEY] = new_context.freeze
63
+ end
64
+
65
+ def clear
66
+ Fiber[FIBER_KEY] = EMPTY_CONTEXT
67
+ end
68
+ end
69
+ end
70
+
71
+ # = Active Support \Event Reporter
72
+ #
73
+ # +ActiveSupport::EventReporter+ provides an interface for reporting structured events to subscribers.
74
+ #
75
+ # To report an event, you can use the +notify+ method:
76
+ #
77
+ # Rails.event.notify("user_created", { id: 123 })
78
+ # # Emits event:
79
+ # # {
80
+ # # name: "user_created",
81
+ # # payload: { id: 123 },
82
+ # # timestamp: 1738964843208679035,
83
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
84
+ # # }
85
+ #
86
+ # The +notify+ API can receive either an event name and a payload hash, or an event object. Names are coerced to strings.
87
+ #
88
+ # === Event Objects
89
+ #
90
+ # If an event object is passed to the +notify+ API, it will be passed through to subscribers as-is, and the name of the
91
+ # object's class will be used as the event name.
92
+ #
93
+ # class UserCreatedEvent
94
+ # def initialize(id:, name:)
95
+ # @id = id
96
+ # @name = name
97
+ # end
98
+ #
99
+ # def serialize
100
+ # {
101
+ # id: @id,
102
+ # name: @name
103
+ # }
104
+ # end
105
+ # end
106
+ #
107
+ # Rails.event.notify(UserCreatedEvent.new(id: 123, name: "John Doe"))
108
+ # # Emits event:
109
+ # # {
110
+ # # name: "UserCreatedEvent",
111
+ # # payload: #<UserCreatedEvent:0x111>,
112
+ # # timestamp: 1738964843208679035,
113
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
114
+ # # }
115
+ #
116
+ # An event is any Ruby object representing a schematized event. While payload hashes allow arbitrary,
117
+ # implicitly-structured data, event objects are intended to enforce a particular schema.
118
+ #
119
+ # Subscribers are responsible for serializing event objects.
120
+ #
121
+ # === Subscribers
122
+ #
123
+ # Subscribers must implement the +emit+ method, which will be called with the event hash.
124
+ #
125
+ # The event hash has the following keys:
126
+ #
127
+ # name: String (The name of the event)
128
+ # payload: Hash, Object (The payload of the event, or the event object itself)
129
+ # tags: Hash (The tags of the event)
130
+ # context: Hash (The context of the event)
131
+ # timestamp: Float (The timestamp of the event, in nanoseconds)
132
+ # source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
133
+ #
134
+ # Subscribers are responsible for encoding events to their desired format before emitting them to their
135
+ # target destination, such as a streaming platform, a log device, or an alerting service.
136
+ #
137
+ # class JSONEventSubscriber
138
+ # def emit(event)
139
+ # json_data = JSON.generate(event)
140
+ # LogExporter.export(json_data)
141
+ # end
142
+ # end
143
+ #
144
+ # class LogSubscriber
145
+ # def emit(event)
146
+ # payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
147
+ # source_location = event[:source_location]
148
+ # log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
149
+ # Rails.logger.info(log)
150
+ # end
151
+ # end
152
+ #
153
+ # Note that event objects are passed through to subscribers as-is, and may need to be serialized before being encoded:
154
+ #
155
+ # class UserCreatedEvent
156
+ # def initialize(id:, name:)
157
+ # @id = id
158
+ # @name = name
159
+ # end
160
+ #
161
+ # def serialize
162
+ # {
163
+ # id: @id,
164
+ # name: @name
165
+ # }
166
+ # end
167
+ # end
168
+ #
169
+ # class LogSubscriber
170
+ # def emit(event)
171
+ # payload = event[:payload]
172
+ # json_data = JSON.generate(payload.serialize)
173
+ # LogExporter.export(json_data)
174
+ # end
175
+ # end
176
+ #
177
+ # ==== Filtered Subscriptions
178
+ #
179
+ # Subscribers can be configured with an optional filter proc to only receive a subset of events:
180
+ #
181
+ # # Only receive events with names starting with "user."
182
+ # Rails.event.subscribe(user_subscriber) { |event| event[:name].start_with?("user.") }
183
+ #
184
+ # # Only receive events with specific payload types
185
+ # Rails.event.subscribe(audit_subscriber) { |event| event[:payload].is_a?(AuditEvent) }
186
+ #
187
+ # === Debug Events
188
+ #
189
+ # You can use the +debug+ method to report an event that will only be reported if the
190
+ # event reporter is in debug mode:
191
+ #
192
+ # Rails.event.debug("my_debug_event", { foo: "bar" })
193
+ #
194
+ # === Tags
195
+ #
196
+ # To add additional context to an event, separate from the event payload, you can add
197
+ # tags via the +tagged+ method:
198
+ #
199
+ # Rails.event.tagged("graphql") do
200
+ # Rails.event.notify("user_created", { id: 123 })
201
+ # end
202
+ #
203
+ # # Emits event:
204
+ # # {
205
+ # # name: "user_created",
206
+ # # payload: { id: 123 },
207
+ # # tags: { graphql: true },
208
+ # # context: {},
209
+ # # timestamp: 1738964843208679035,
210
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
211
+ # # }
212
+ #
213
+ # === Context Store
214
+ #
215
+ # You may want to attach metadata to every event emitted by the reporter. While tags
216
+ # provide domain-specific context for a series of events, context is scoped to the job / request
217
+ # and should be used for metadata associated with the execution context.
218
+ # Context can be set via the +set_context+ method:
219
+ #
220
+ # Rails.event.set_context(request_id: "abcd123", user_agent: "TestAgent")
221
+ # Rails.event.notify("user_created", { id: 123 })
222
+ #
223
+ # # Emits event:
224
+ # # {
225
+ # # name: "user_created",
226
+ # # payload: { id: 123 },
227
+ # # tags: {},
228
+ # # context: { request_id: "abcd123", user_agent: TestAgent" },
229
+ # # timestamp: 1738964843208679035,
230
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
231
+ # # }
232
+ #
233
+ # Context is reset automatically before and after each request.
234
+ #
235
+ # A custom context store can be configured via +config.active_support.event_reporter_context_store+.
236
+ #
237
+ # # config/application.rb
238
+ # config.active_support.event_reporter_context_store = CustomContextStore
239
+ #
240
+ # class CustomContextStore
241
+ # class << self
242
+ # def context
243
+ # # Return the context.
244
+ # end
245
+ #
246
+ # def set_context(context_hash)
247
+ # # Append context_hash to the existing context store.
248
+ # end
249
+ #
250
+ # def clear
251
+ # # Delete the stored context.
252
+ # end
253
+ # end
254
+ # end
255
+ #
256
+ # The Event Reporter standardizes on symbol keys for all payload data, tags, and context store entries.
257
+ # String keys are automatically converted to symbols for consistency.
258
+ #
259
+ # Rails.event.notify("user.created", { "id" => 123 })
260
+ # # Emits event:
261
+ # # {
262
+ # # name: "user.created",
263
+ # # payload: { id: 123 },
264
+ # # }
265
+ #
266
+ # === Security
267
+ #
268
+ # When reporting events, Hash-based payloads are automatically filtered to remove sensitive data based on {Rails.application.filter_parameters}[https://guides.rubyonrails.org/configuring.html#config-filter-parameters].
269
+ #
270
+ # If an {event object}[rdoc-ref:EventReporter@Event+Objects] is given instead, subscribers will need to filter sensitive data themselves, e.g. with ActiveSupport::ParameterFilter.
271
+ class EventReporter
272
+ # Sets whether to raise an error if a subscriber raises an error during
273
+ # event emission, or when unexpected arguments are passed to +notify+.
274
+ attr_writer :raise_on_error
275
+
276
+ attr_writer :debug_mode # :nodoc:
277
+
278
+ attr_reader :subscribers # :nodoc
279
+
280
+ class << self
281
+ attr_accessor :context_store # :nodoc:
282
+ end
283
+
284
+ self.context_store = EventContext
285
+
286
+ def initialize(*subscribers, raise_on_error: false)
287
+ @subscribers = []
288
+ subscribers.each { |subscriber| subscribe(subscriber) }
289
+ @debug_mode = false
290
+ @raise_on_error = raise_on_error
291
+ end
292
+
293
+ # Registers a new event subscriber. The subscriber must respond to
294
+ #
295
+ # emit(event: Hash)
296
+ #
297
+ # The event hash will have the following keys:
298
+ #
299
+ # name: String (The name of the event)
300
+ # payload: Hash, Object (The payload of the event, or the event object itself)
301
+ # tags: Hash (The tags of the event)
302
+ # context: Hash (The context of the event)
303
+ # timestamp: Float (The timestamp of the event, in nanoseconds)
304
+ # source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
305
+ #
306
+ # An optional filter proc can be provided to only receive a subset of events:
307
+ #
308
+ # Rails.event.subscribe(subscriber) { |event| event[:name].start_with?("user.") }
309
+ # Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }
310
+ #
311
+ def subscribe(subscriber, &filter)
312
+ unless subscriber.respond_to?(:emit)
313
+ raise ArgumentError, "Event subscriber #{subscriber.class.name} must respond to #emit"
314
+ end
315
+ @subscribers << { subscriber: subscriber, filter: filter }
316
+ end
317
+
318
+ # Unregister an event subscriber. Accepts either a subscriber or a class.
319
+ #
320
+ # subscriber = MyEventSubscriber.new
321
+ # Rails.event.subscribe(subscriber)
322
+ #
323
+ # Rails.event.unsubscribe(subscriber)
324
+ # # or
325
+ # Rails.event.unsubscribe(MyEventSubscriber)
326
+ def unsubscribe(subscriber)
327
+ @subscribers.delete_if { |s| subscriber === s[:subscriber] }
328
+ end
329
+
330
+ # Reports an event to all registered subscribers. An event name and payload can be provided:
331
+ #
332
+ # Rails.event.notify("user.created", { id: 123 })
333
+ # # Emits event:
334
+ # # {
335
+ # # name: "user.created",
336
+ # # payload: { id: 123 },
337
+ # # tags: {},
338
+ # # context: {},
339
+ # # timestamp: 1738964843208679035,
340
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
341
+ # # }
342
+ #
343
+ # Alternatively, an event object can be provided:
344
+ #
345
+ # Rails.event.notify(UserCreatedEvent.new(id: 123))
346
+ # # Emits event:
347
+ # # {
348
+ # # name: "UserCreatedEvent",
349
+ # # payload: #<UserCreatedEvent:0x111>,
350
+ # # tags: {},
351
+ # # context: {},
352
+ # # timestamp: 1738964843208679035,
353
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
354
+ # # }
355
+ #
356
+ # ==== Arguments
357
+ #
358
+ # * +:payload+ - The event payload when using string/symbol event names.
359
+ #
360
+ # * +:caller_depth+ - The stack depth to use for source location (default: 1).
361
+ #
362
+ # * +:kwargs+ - Additional payload data when using string/symbol event names.
363
+ def notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)
364
+ name = resolve_name(name_or_object)
365
+ payload = resolve_payload(name_or_object, payload, **kwargs)
366
+
367
+ event = {
368
+ name: name,
369
+ payload: payload,
370
+ tags: TagStack.tags,
371
+ context: context_store.context,
372
+ timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond),
373
+ }
374
+
375
+ caller_location = caller_locations(caller_depth, 1)&.first
376
+
377
+ if caller_location
378
+ source_location = {
379
+ filepath: caller_location.path,
380
+ lineno: caller_location.lineno,
381
+ label: caller_location.label,
382
+ }
383
+ event[:source_location] = source_location
384
+ end
385
+
386
+ @subscribers.each do |subscriber_entry|
387
+ subscriber = subscriber_entry[:subscriber]
388
+ filter = subscriber_entry[:filter]
389
+
390
+ next if filter && !filter.call(event)
391
+
392
+ subscriber.emit(event)
393
+ rescue => subscriber_error
394
+ if raise_on_error?
395
+ raise
396
+ else
397
+ ActiveSupport.error_reporter.report(subscriber_error, handled: true)
398
+ end
399
+ end
400
+
401
+ nil
402
+ end
403
+
404
+ # Temporarily enables debug mode for the duration of the block.
405
+ # Calls to +debug+ will only be reported if debug mode is enabled.
406
+ #
407
+ # Rails.event.with_debug do
408
+ # Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
409
+ # end
410
+ def with_debug
411
+ prior = Fiber[:event_reporter_debug_mode]
412
+ Fiber[:event_reporter_debug_mode] = true
413
+ yield
414
+ ensure
415
+ Fiber[:event_reporter_debug_mode] = prior
416
+ end
417
+
418
+ # Check if debug mode is currently enabled. Debug mode is enabled on the reporter
419
+ # via +with_debug+, and in local environments.
420
+ def debug_mode?
421
+ @debug_mode || Fiber[:event_reporter_debug_mode]
422
+ end
423
+
424
+ # Report an event only when in debug mode. For example:
425
+ #
426
+ # Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
427
+ #
428
+ # ==== Arguments
429
+ #
430
+ # * +:payload+ - The event payload when using string/symbol event names.
431
+ #
432
+ # * +:caller_depth+ - The stack depth to use for source location (default: 1).
433
+ #
434
+ # * +:kwargs+ - Additional payload data when using string/symbol event names.
435
+ def debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)
436
+ if debug_mode?
437
+ if block_given?
438
+ notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs.merge(yield))
439
+ else
440
+ notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs)
441
+ end
442
+ end
443
+ end
444
+
445
+ # Add tags to events to supply additional context. Tags operate in a stack-oriented manner,
446
+ # so all events emitted within the block inherit the same set of tags. For example:
447
+ #
448
+ # Rails.event.tagged("graphql") do
449
+ # Rails.event.notify("user.created", { id: 123 })
450
+ # end
451
+ #
452
+ # # Emits event:
453
+ # # {
454
+ # # name: "user.created",
455
+ # # payload: { id: 123 },
456
+ # # tags: { graphql: true },
457
+ # # context: {},
458
+ # # timestamp: 1738964843208679035,
459
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
460
+ # # }
461
+ #
462
+ # Tags can be provided as arguments or as keyword arguments, and can be nested:
463
+ #
464
+ # Rails.event.tagged("graphql") do
465
+ # # Other code here...
466
+ # Rails.event.tagged(section: "admin") do
467
+ # Rails.event.notify("user.created", { id: 123 })
468
+ # end
469
+ # end
470
+ #
471
+ # # Emits event:
472
+ # # {
473
+ # # name: "user.created",
474
+ # # payload: { id: 123 },
475
+ # # tags: { section: "admin", graphql: true },
476
+ # # context: {},
477
+ # # timestamp: 1738964843208679035,
478
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
479
+ # # }
480
+ #
481
+ # The +tagged+ API can also receive a tag object:
482
+ #
483
+ # graphql_tag = GraphqlTag.new(operation_name: "user_created", operation_type: "mutation")
484
+ # Rails.event.tagged(graphql_tag) do
485
+ # Rails.event.notify("user.created", { id: 123 })
486
+ # end
487
+ #
488
+ # # Emits event:
489
+ # # {
490
+ # # name: "user.created",
491
+ # # payload: { id: 123 },
492
+ # # tags: { "GraphqlTag": #<GraphqlTag:0x111> },
493
+ # # context: {},
494
+ # # timestamp: 1738964843208679035,
495
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
496
+ # # }
497
+ def tagged(*args, **kwargs, &block)
498
+ TagStack.with_tags(*args, **kwargs, &block)
499
+ end
500
+
501
+ # Sets context data that will be included with all events emitted by the reporter.
502
+ # Context data should be scoped to the job or request, and is reset automatically
503
+ # before and after each request and job.
504
+ #
505
+ # Rails.event.set_context(user_agent: "TestAgent")
506
+ # Rails.event.set_context(job_id: "abc123")
507
+ # Rails.event.tagged("graphql") do
508
+ # Rails.event.notify("user_created", { id: 123 })
509
+ # end
510
+ #
511
+ # # Emits event:
512
+ # # {
513
+ # # name: "user_created",
514
+ # # payload: { id: 123 },
515
+ # # tags: { graphql: true },
516
+ # # context: { user_agent: "TestAgent", job_id: "abc123" },
517
+ # # timestamp: 1738964843208679035
518
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
519
+ # # }
520
+ def set_context(context)
521
+ context_store.set_context(context)
522
+ end
523
+
524
+ # Clears all context data.
525
+ def clear_context
526
+ context_store.clear
527
+ end
528
+
529
+ # Returns the current context data.
530
+ def context
531
+ context_store.context
532
+ end
533
+
534
+ def reload_payload_filter # :nodoc:
535
+ @payload_filter = nil
536
+ payload_filter
537
+ end
538
+
539
+ private
540
+ def raise_on_error?
541
+ @raise_on_error
542
+ end
543
+
544
+ def context_store
545
+ self.class.context_store
546
+ end
547
+
548
+ def payload_filter
549
+ @payload_filter ||= begin
550
+ mask = ActiveSupport::ParameterFilter::FILTERED
551
+ ActiveSupport::ParameterFilter.new(ActiveSupport.filter_parameters, mask: mask)
552
+ end
553
+ end
554
+
555
+ def resolve_name(name_or_object)
556
+ case name_or_object
557
+ when String, Symbol
558
+ name_or_object.to_s
559
+ else
560
+ name_or_object.class.name
561
+ end
562
+ end
563
+
564
+ def resolve_payload(name_or_object, payload, **kwargs)
565
+ case name_or_object
566
+ when String, Symbol
567
+ handle_unexpected_args(name_or_object, payload, kwargs) if payload && kwargs.any?
568
+ if kwargs.any?
569
+ payload_filter.filter(kwargs.transform_keys(&:to_sym))
570
+ elsif payload
571
+ payload_filter.filter(payload.transform_keys(&:to_sym))
572
+ end
573
+ else
574
+ handle_unexpected_args(name_or_object, payload, kwargs) if payload || kwargs.any?
575
+ name_or_object
576
+ end
577
+ end
578
+
579
+ def handle_unexpected_args(name_or_object, payload, kwargs)
580
+ message = <<~MESSAGE
581
+ Rails.event.notify accepts either an event object, a payload hash, or keyword arguments.
582
+ Received: #{name_or_object.inspect}, #{payload.inspect}, #{kwargs.inspect}
583
+ MESSAGE
584
+
585
+ if raise_on_error?
586
+ raise ArgumentError, message
587
+ else
588
+ ActiveSupport.error_reporter.report(ArgumentError.new(message), handled: true)
589
+ end
590
+ end
591
+ end
592
+ end
@@ -73,9 +73,13 @@ module ActiveSupport
73
73
  attr_reader :updated, :files
74
74
 
75
75
  def initialize(files, dirs)
76
- @files = files.map { |file| Pathname(file).expand_path }.to_set
76
+ gem_paths = Gem.path
77
+ files = files.map { |f| Pathname(f).expand_path }
78
+ files.reject! { |f| f.to_s.start_with?(*gem_paths) }
79
+ @files = files.to_set
77
80
 
78
81
  @dirs = dirs.each_with_object({}) do |(dir, exts), hash|
82
+ next if dir.start_with?(*gem_paths)
79
83
  hash[Pathname(dir).expand_path] = Array(exts).map { |ext| ext.to_s.sub(/\A\.?/, ".") }.to_set
80
84
  end
81
85
 
@@ -2,8 +2,41 @@
2
2
 
3
3
  module ActiveSupport
4
4
  module ExecutionContext # :nodoc:
5
+ class Record
6
+ attr_reader :store, :current_attributes_instances
7
+
8
+ def initialize
9
+ @store = {}
10
+ @current_attributes_instances = {}
11
+ @stack = []
12
+ end
13
+
14
+ def push
15
+ @stack << @store << @current_attributes_instances
16
+ @store = {}
17
+ @current_attributes_instances = {}
18
+ self
19
+ end
20
+
21
+ def pop
22
+ @current_attributes_instances = @stack.pop
23
+ @store = @stack.pop
24
+ self
25
+ end
26
+ end
27
+
5
28
  @after_change_callbacks = []
29
+
30
+ # Execution context nesting should only legitimately happen during test
31
+ # because the test case itself is wrapped in an executor, and it might call
32
+ # into a controller or job which should be executed with their own fresh context.
33
+ # However in production this should never happen, and for extra safety we make sure to
34
+ # fully clear the state at the end of the request or job cycle.
35
+ @nestable = false
36
+
6
37
  class << self
38
+ attr_accessor :nestable
39
+
7
40
  def after_change(&block)
8
41
  @after_change_callbacks << block
9
42
  end
@@ -14,9 +47,11 @@ module ActiveSupport
14
47
  options.symbolize_keys!
15
48
  keys = options.keys
16
49
 
17
- store = self.store
50
+ store = record.store
18
51
 
19
- previous_context = keys.zip(store.values_at(*keys)).to_h
52
+ previous_context = if block_given?
53
+ keys.zip(store.values_at(*keys)).to_h
54
+ end
20
55
 
21
56
  store.merge!(options)
22
57
  @after_change_callbacks.each(&:call)
@@ -32,21 +67,43 @@ module ActiveSupport
32
67
  end
33
68
 
34
69
  def []=(key, value)
35
- store[key.to_sym] = value
70
+ record.store[key.to_sym] = value
36
71
  @after_change_callbacks.each(&:call)
37
72
  end
38
73
 
39
74
  def to_h
40
- store.dup
75
+ record.store.dup
76
+ end
77
+
78
+ def push
79
+ if @nestable
80
+ record.push
81
+ else
82
+ clear
83
+ end
84
+ self
85
+ end
86
+
87
+ def pop
88
+ if @nestable
89
+ record.pop
90
+ else
91
+ clear
92
+ end
93
+ self
41
94
  end
42
95
 
43
96
  def clear
44
- store.clear
97
+ IsolatedExecutionState[:active_support_execution_context] = nil
98
+ end
99
+
100
+ def current_attributes_instances
101
+ record.current_attributes_instances
45
102
  end
46
103
 
47
104
  private
48
- def store
49
- IsolatedExecutionState[:active_support_execution_context] ||= {}
105
+ def record
106
+ IsolatedExecutionState[:active_support_execution_context] ||= Record.new
50
107
  end
51
108
  end
52
109
  end