action_reporter 2.0.2 → 3.0.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: aabef092878a25b94f003857a4490ae941458a23f10a645f90967c2410f6abfd
4
- data.tar.gz: 26e8cda7492641af36a02b4320a415fc1b1afabb530f7dcb06f475c8ece39d43
3
+ metadata.gz: 883155825258f4bf10900efaa4f5486fa651ddda415e21282ceba8063909fa1b
4
+ data.tar.gz: 7b2fe04f7a946503e516e744a9fa292b8002d6b75a30b69830f79ff272c26d7f
5
5
  SHA512:
6
- metadata.gz: 490769c5999f15251385b654ec9a0e8e65b11b8f50e4f3c21d974b72bc3d2d8036852d9dfab8f108931b3809ddf9e9ef9dd7265bd88adc710febe91ef659c969
7
- data.tar.gz: fbef1b1338b491ba88f792cb5dc1de0a218b4b7d914430ae3f607bfaccd7bd3cb0eb1d2b2694a80c2417b75748a91043d34efc2a034b3944fefd640f37bfe63c
6
+ metadata.gz: a252069094c66d8c5bf87204101a426c5dec49496065ba18548ab00a0c304932fc247fd32c13e08c34cc276ef1b777937c5456b895b58b6e42235b753c41b557
7
+ data.tar.gz: 9e596e6be356fd140c3137b90952dd8c105907fd17aac78a3e8ec21586eaa074cb760477b9b17198fec246473f3d32cd8abe0eb4691a596562ef742deb847f25
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.0.0 (2026-03-13)
4
+
5
+ - BREAKING: Remove backward-compat reporter loading
6
+ - Remove `AVAILABLE_REPORTERS` constant
7
+ - Stop eager requiring built-in reporters at boot
8
+ - Built-in reporters are now loaded lazily through `ActionReporter.available_reporters`
9
+ - Fix built-in reporter constant autoloading
10
+ - Add `autoload` mappings for built-in reporters (`RailsReporter`, `AuditedReporter`, `PaperTrailReporter`, `ActiveVersionReporter`, `SentryReporter`, `HoneybadgerReporter`, `ScoutApmReporter`)
11
+ - Fix `NameError: uninitialized constant ActionReporter::ActiveVersionReporter` when reporters are referenced directly from app initializers
12
+ - Fix custom reporter lazy loading regression
13
+ - `PluginDiscovery.load_registered_reporter` now attempts `require` before constant lookup
14
+ - Registered reporters can now be loaded from `require_path` when class is not preloaded
15
+ - Improve plugin discovery thread safety
16
+ - Synchronize `register` writes with discovery mutex
17
+ - Snapshot registered reporters under lock in `available_reporters` before iteration
18
+ - Remove fragile loaded-feature detection
19
+ - Remove substring-based `$LOADED_FEATURES` matching that could produce false positives
20
+ - Rely on idempotent `require` behavior instead
21
+ - Improve context transformation behavior
22
+ - `Utils.deep_transform_values` now recursively transforms all values, including objects nested directly inside arrays
23
+ - Add configurable current-context storage adapter
24
+ - New `ActionReporter::Current.storage_adapter=` with `#[]` / `#[]=` contract
25
+ - New `ActionReporter::Current.reset_storage_adapter!`
26
+ - Storage resolution order: explicit `storage_adapter` -> `ActiveSupport::IsolatedExecutionState` -> `Thread.current`
27
+ - Enables fiber-aware request storage customization in non-Rails environments
28
+ - Add regression coverage for lazy loading and storage behavior
29
+ - Add spec for loading registered reporter via `require_path` when class is not preloaded
30
+ - Add spec for transforming array-contained objects in deep context transform
31
+ - Add specs for `Current.storage_adapter` validation and precedence
32
+
3
33
  ## 2.0.2 (2026-03-13)
4
34
 
5
35
  - Add `ActiveVersionReporter` integration for the `active_version` gem
@@ -2,57 +2,96 @@ require_relative "error"
2
2
 
3
3
  module ActionReporter
4
4
  # Thread-safe storage for ActionReporter context attributes
5
- # Uses Thread.current for maximum compatibility and thread safety
6
- # Note: ActiveSupport::CurrentAttributes is request-scoped (not thread-scoped),
7
- # so we use Thread.current for proper thread isolation
5
+ # Uses ActiveSupport::IsolatedExecutionState when available (fiber/request-aware),
6
+ # and falls back to Thread.current for non-Rails/non-ActiveSupport environments.
8
7
  class Current
8
+ STORAGE_PREFIX = :action_reporter
9
+
9
10
  class << self
11
+ attr_reader :storage_adapter
12
+
13
+ def storage_adapter=(adapter)
14
+ if adapter && (!adapter.respond_to?(:[]) || !adapter.respond_to?(:[]=))
15
+ raise ArgumentError, "storage_adapter must respond to #[] and #[]="
16
+ end
17
+
18
+ @storage_adapter = adapter
19
+ end
20
+
21
+ def reset_storage_adapter!
22
+ @storage_adapter = nil
23
+ end
24
+
10
25
  def current_user
11
- Thread.current[:action_reporter_current_user]
26
+ read(:current_user)
12
27
  end
13
28
 
14
29
  def current_user=(user)
15
- Thread.current[:action_reporter_current_user] = user
30
+ write(:current_user, user)
16
31
  end
17
32
 
18
33
  def current_request_uuid
19
- Thread.current[:action_reporter_current_request_uuid]
34
+ read(:current_request_uuid)
20
35
  end
21
36
 
22
37
  def current_request_uuid=(uuid)
23
- Thread.current[:action_reporter_current_request_uuid] = uuid
38
+ write(:current_request_uuid, uuid)
24
39
  end
25
40
 
26
41
  def current_remote_addr
27
- Thread.current[:action_reporter_current_remote_addr]
42
+ read(:current_remote_addr)
28
43
  end
29
44
 
30
45
  def current_remote_addr=(addr)
31
- Thread.current[:action_reporter_current_remote_addr] = addr
46
+ write(:current_remote_addr, addr)
32
47
  end
33
48
 
34
49
  def transaction_id
35
- Thread.current[:action_reporter_transaction_id]
50
+ read(:transaction_id)
36
51
  end
37
52
 
38
53
  def transaction_id=(transaction_id)
39
- Thread.current[:action_reporter_transaction_id] = transaction_id
54
+ write(:transaction_id, transaction_id)
40
55
  end
41
56
 
42
57
  def transaction_name
43
- Thread.current[:action_reporter_transaction_name]
58
+ read(:transaction_name)
44
59
  end
45
60
 
46
61
  def transaction_name=(transaction_name)
47
- Thread.current[:action_reporter_transaction_name] = transaction_name
62
+ write(:transaction_name, transaction_name)
48
63
  end
49
64
 
50
65
  def reset
51
- Thread.current[:action_reporter_current_user] = nil
52
- Thread.current[:action_reporter_current_request_uuid] = nil
53
- Thread.current[:action_reporter_current_remote_addr] = nil
54
- Thread.current[:action_reporter_transaction_id] = nil
55
- Thread.current[:action_reporter_transaction_name] = nil
66
+ write(:current_user, nil)
67
+ write(:current_request_uuid, nil)
68
+ write(:current_remote_addr, nil)
69
+ write(:transaction_id, nil)
70
+ write(:transaction_name, nil)
71
+ end
72
+
73
+ private
74
+
75
+ def storage
76
+ return storage_adapter if storage_adapter
77
+
78
+ if defined?(ActiveSupport::IsolatedExecutionState)
79
+ ActiveSupport::IsolatedExecutionState
80
+ else
81
+ Thread.current
82
+ end
83
+ end
84
+
85
+ def key(name)
86
+ :"#{STORAGE_PREFIX}_#{name}"
87
+ end
88
+
89
+ def read(name)
90
+ storage[key(name)]
91
+ end
92
+
93
+ def write(name, value)
94
+ storage[key(name)] = value
56
95
  end
57
96
  end
58
97
  end
@@ -28,10 +28,12 @@ module ActionReporter
28
28
  # @param class_name [String] Fully qualified class name
29
29
  # @param require_path [String] Path to require (e.g., "action_reporter/custom_reporter")
30
30
  def register(name, class_name:, require_path:)
31
- registered_reporters[name] = {
32
- class_name: class_name,
33
- require_path: require_path
34
- }
31
+ discovery_lock.synchronize do
32
+ registered_reporters[name] = {
33
+ class_name: class_name,
34
+ require_path: require_path
35
+ }
36
+ end
35
37
  end
36
38
 
37
39
  # Discover reporters from the filesystem (lazy-loaded, cached)
@@ -66,7 +68,8 @@ module ActionReporter
66
68
  reporters = discover.dup
67
69
 
68
70
  # Add registered reporters (lazy-loaded)
69
- registered_reporters.each_value do |config|
71
+ registered_configs = discovery_lock.synchronize { registered_reporters.values.dup }
72
+ registered_configs.each do |config|
70
73
  reporter_class = load_registered_reporter(config)
71
74
  reporters << reporter_class if reporter_class
72
75
  rescue => e
@@ -99,7 +102,15 @@ module ActionReporter
99
102
 
100
103
  full_class_name = "ActionReporter::#{class_name}"
101
104
 
102
- # Check if class is already defined (files are pre-required at boot)
105
+ # Lazy load reporter file on discovery.
106
+ # Some callers (including tests) pass synthetic paths that do not exist.
107
+ begin
108
+ require file_path
109
+ rescue LoadError
110
+ # Ignore and continue with constant resolution.
111
+ end
112
+
113
+ # Check if class is defined after requiring.
103
114
  return nil unless Object.const_defined?(full_class_name)
104
115
 
105
116
  klass = Object.const_get(full_class_name)
@@ -109,30 +120,22 @@ module ActionReporter
109
120
  end
110
121
 
111
122
  def load_registered_reporter(config)
112
- # Check if class is already defined (e.g., in tests or already loaded)
113
- return nil unless Object.const_defined?(config[:class_name])
114
-
115
- klass = Object.const_get(config[:class_name])
116
- return nil unless klass < Base
117
-
118
- # Only require if class is not already available and require_path is provided
119
- # This allows classes defined inline (e.g., in tests) to work without requiring files
120
- if config[:require_path] && !required?(config[:require_path])
123
+ # Try to load reporter file first; require is idempotent.
124
+ if config[:require_path]
121
125
  begin
122
126
  require config[:require_path]
123
127
  rescue LoadError => e
124
- # If file doesn't exist but class is already defined, that's okay
125
- # (e.g., class defined inline in tests or already loaded)
126
128
  warn "ActionReporter: Could not require #{config[:require_path]}: #{e.message}" if logger
127
- # Continue - class might already be defined
128
129
  end
129
130
  end
130
131
 
131
- klass
132
- end
132
+ # Check if class is defined (e.g., file loaded, inline test class, or already loaded)
133
+ return nil unless Object.const_defined?(config[:class_name])
133
134
 
134
- def required?(path)
135
- $LOADED_FEATURES.any? { |feature| feature.include?(path) }
135
+ klass = Object.const_get(config[:class_name])
136
+ return nil unless klass < Base
137
+
138
+ klass
136
139
  end
137
140
 
138
141
  def logger
@@ -2,17 +2,16 @@ module ActionReporter
2
2
  module Utils
3
3
  module_function
4
4
 
5
- def deep_transform_values(hash, &block)
6
- hash.each_with_object({}) do |(k, v), result|
7
- value = if v.is_a?(Hash)
8
- deep_transform_values(v, &block)
9
- elsif v.is_a?(Array)
10
- v.map { |e| e.is_a?(Hash) ? deep_transform_values(e, &block) : e }
11
- else
12
- v
5
+ def deep_transform_values(value, &block)
6
+ case value
7
+ when Hash
8
+ value.each_with_object({}) do |(k, v), result|
9
+ result[k] = deep_transform_values(v, &block)
13
10
  end
14
-
15
- result[k] = block.call(value)
11
+ when Array
12
+ value.map { |element| deep_transform_values(element, &block) }
13
+ else
14
+ block.call(value)
16
15
  end
17
16
  end
18
17
  end
@@ -1,3 +1,3 @@
1
1
  module ActionReporter
2
- VERSION = "2.0.2"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -4,30 +4,16 @@ require "action_reporter/base"
4
4
  require "action_reporter/current"
5
5
  require "action_reporter/plugin_discovery"
6
6
 
7
- # Core reporters are still required for backward compatibility
8
- # But discovery mechanism allows for lazy loading and custom reporters
9
- require "action_reporter/rails_reporter"
10
- require "action_reporter/honeybadger_reporter"
11
- require "action_reporter/sentry_reporter"
12
- require "action_reporter/scout_apm_reporter"
13
- require "action_reporter/audited_reporter"
14
- require "action_reporter/paper_trail_reporter"
15
- require "action_reporter/active_version_reporter"
16
-
17
7
  module ActionReporter
18
- module_function
8
+ autoload :RailsReporter, "action_reporter/rails_reporter"
9
+ autoload :AuditedReporter, "action_reporter/audited_reporter"
10
+ autoload :PaperTrailReporter, "action_reporter/paper_trail_reporter"
11
+ autoload :ActiveVersionReporter, "action_reporter/active_version_reporter"
12
+ autoload :SentryReporter, "action_reporter/sentry_reporter"
13
+ autoload :HoneybadgerReporter, "action_reporter/honeybadger_reporter"
14
+ autoload :ScoutApmReporter, "action_reporter/scout_apm_reporter"
19
15
 
20
- # Legacy hardcoded list (maintained for backward compatibility)
21
- # Use `available_reporters` for auto-discovered reporters
22
- AVAILABLE_REPORTERS = [
23
- ActionReporter::RailsReporter,
24
- ActionReporter::HoneybadgerReporter,
25
- ActionReporter::SentryReporter,
26
- ActionReporter::ScoutApmReporter,
27
- ActionReporter::AuditedReporter,
28
- ActionReporter::PaperTrailReporter,
29
- ActionReporter::ActiveVersionReporter
30
- ].freeze
16
+ module_function
31
17
 
32
18
  # Get available reporters (auto-discovered + registered)
33
19
  # This is lazy-loaded and does not block application boot
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov