action_reporter 2.0.2 → 3.1.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: 1ee447f96cc8b2c64df7f7f824b49cd7b5fbbbb591496b507c1a352883468701
4
+ data.tar.gz: 777dd9085576844c30d5a0a81ce57933072be54653c9c2326c041c7de61d796f
5
5
  SHA512:
6
- metadata.gz: 490769c5999f15251385b654ec9a0e8e65b11b8f50e4f3c21d974b72bc3d2d8036852d9dfab8f108931b3809ddf9e9ef9dd7265bd88adc710febe91ef659c969
7
- data.tar.gz: fbef1b1338b491ba88f792cb5dc1de0a218b4b7d914430ae3f607bfaccd7bd3cb0eb1d2b2694a80c2417b75748a91043d34efc2a034b3944fefd640f37bfe63c
6
+ metadata.gz: 52bf9687ec703a12869bef96419ae1873b36d178d1e5cab06d5683dcd048ef1b49066efe0b7f2c1128d64b8bc762eafc89c1890767b900d49dcbc8c35f6e67e1
7
+ data.tar.gz: 94debcd931e0307f881721f94a13d493f65f4e2fc5de61d11ce6ad8a0536e6a2325646f4241613ef80be3a7036fd8608854a7ec22a727cff1890f8aebf890307
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.1.0 (2026-03-27)
4
+
5
+ - Add `ActionReporter.user_id_resolver` for configuring how to resolve user IDs
6
+ - Improve ScoutApmReporter current user setter to use `id` instead of `user_global_id`
7
+ - Improve HoneybadgerReporter current user setter to use `user_id` instead of `user_global_id`
8
+ - Improve SentryReporter current user setter to use `id` instead of `user_global_id` and fix `transaction_id` setter to use `transaction_id` instead of `transactionId`
9
+
10
+ ## 3.0.0 (2026-03-13)
11
+
12
+ - BREAKING: Remove backward-compat reporter loading
13
+ - Remove `AVAILABLE_REPORTERS` constant
14
+ - Stop eager requiring built-in reporters at boot
15
+ - Built-in reporters are now loaded lazily through `ActionReporter.available_reporters`
16
+ - Improve plugin discovery thread safety
17
+ - Synchronize `register` writes with discovery mutex
18
+ - Snapshot registered reporters under lock in `available_reporters` before iteration
19
+ - Remove fragile loaded-feature detection
20
+ - Remove substring-based `$LOADED_FEATURES` matching that could produce false positives
21
+ - Rely on idempotent `require` behavior instead
22
+ - Improve context transformation behavior
23
+ - `Utils.deep_transform_values` now recursively transforms all values, including objects nested directly inside arrays
24
+ - Add configurable current-context storage adapter
25
+ - New `ActionReporter::Current.storage_adapter=` with `#[]` / `#[]=` contract
26
+ - New `ActionReporter::Current.reset_storage_adapter!`
27
+ - Storage resolution order: explicit `storage_adapter` -> `ActiveSupport::IsolatedExecutionState` -> `Thread.current`
28
+ - Enables fiber-aware request storage customization in non-Rails environments
29
+
3
30
  ## 2.0.2 (2026-03-13)
4
31
 
5
32
  - Add `ActiveVersionReporter` integration for the `active_version` gem
data/README.md CHANGED
@@ -1,18 +1,11 @@
1
1
  # action_reporter
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/action_reporter.svg)](https://badge.fury.io/rb/action_reporter) [![Test Status](https://github.com/amkisko/action_reporter.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/action_reporter.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/action_reporter.rb/graph/badge.svg?token=JCV2A7NWTE)](https://codecov.io/gh/amkisko/action_reporter.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_action_reporter.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_action_reporter.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/action_reporter.svg?v=3.0.0)](https://badge.fury.io/rb/action_reporter) [![Test Status](https://github.com/amkisko/action_reporter.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/action_reporter.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/action_reporter.rb/graph/badge.svg?token=JCV2A7NWTE)](https://codecov.io/gh/amkisko/action_reporter.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_action_reporter.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_action_reporter.rb)
4
4
 
5
5
  Ruby wrapper for multiple reporting services.
6
6
 
7
7
  Supported services: Rails logger, gem audited, gem PaperTrail, gem ActiveVersion, Sentry, Honeybadger, scoutapm.
8
8
 
9
- Sponsored by [Kisko Labs](https://www.kiskolabs.com).
10
-
11
- <a href="https://www.kiskolabs.com">
12
- <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
13
- </a>
14
-
15
-
16
9
  ## Installation
17
10
 
18
11
  Add to your Gemfile:
@@ -173,3 +166,11 @@ gem push action_reporter-*.gem
173
166
  ## License
174
167
 
175
168
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
169
+
170
+ ## Sponsors
171
+
172
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
173
+
174
+ <a href="https://www.kiskolabs.com">
175
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
176
+ </a>
@@ -48,5 +48,28 @@ module ActionReporter
48
48
 
49
49
  def transaction_name=(transaction_name)
50
50
  end
51
+
52
+ def resolve_user_id(user)
53
+ resolver = ActionReporter.user_id_resolver
54
+ if resolver.respond_to?(:call)
55
+ resolver.call(user)
56
+ else
57
+ default_resolve_user_id(user)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def default_resolve_user_id(user)
64
+ if defined?(::GlobalID) && user.is_a?(::GlobalID)
65
+ user.to_s
66
+ elsif defined?(::ActiveRecord::Base) && user.is_a?(::ActiveRecord::Base)
67
+ (user.try(:to_global_id) || user.try(:id)).to_s
68
+ elsif user.respond_to?(:to_global_id)
69
+ user.to_global_id.to_s
70
+ else
71
+ user.to_s
72
+ end
73
+ end
51
74
  end
52
75
  end
@@ -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
@@ -22,7 +22,8 @@ module ActionReporter
22
22
  end
23
23
 
24
24
  def current_user=(user)
25
- honeybadger_class.context(user_global_id: user.to_global_id.to_s) if user
25
+ id = resolve_user_id(user)
26
+ honeybadger_class.context(user_id: id)
26
27
  end
27
28
  end
28
29
  end
@@ -6,7 +6,7 @@ module ActionReporter
6
6
  module PluginDiscovery
7
7
  class << self
8
8
  # Initialize class instance variables
9
- @registered_reporters = {}
9
+ @registered_reporters = nil
10
10
  @discovered_reporters = nil
11
11
  @discovery_lock = Mutex.new
12
12
 
@@ -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
@@ -13,8 +13,13 @@ module ActionReporter
13
13
  scoutapm_context_class.add(new_context)
14
14
  end
15
15
 
16
+ def current_remote_addr=(remote_addr)
17
+ scoutapm_context_class.add_user(ip: remote_addr)
18
+ end
19
+
16
20
  def current_user=(user)
17
- scoutapm_context_class.add_user(user_global_id: user&.to_global_id&.to_s)
21
+ id = resolve_user_id(user)
22
+ scoutapm_context_class.add_user(id: id)
18
23
  end
19
24
 
20
25
  def ignore_transaction!
@@ -23,11 +23,12 @@ module ActionReporter
23
23
  end
24
24
 
25
25
  def current_user=(user)
26
- sentry_class.set_user(user_global_id: user&.to_global_id&.to_s)
26
+ id = resolve_user_id(user)
27
+ sentry_class.set_user(id: id)
27
28
  end
28
29
 
29
30
  def transaction_id=(transaction_id)
30
- sentry_class.set_tags(transactionId: transaction_id)
31
+ sentry_class.set_tags(transaction_id: transaction_id)
31
32
  end
32
33
 
33
34
  def transaction_name=(transaction_name)
@@ -35,5 +36,11 @@ module ActionReporter
35
36
  scope.set_transaction_name(transaction_name)
36
37
  end
37
38
  end
39
+
40
+ private
41
+
42
+ def blank_user_id?(id)
43
+ id.nil? || (id.respond_to?(:empty?) && id.empty?)
44
+ end
38
45
  end
39
46
  end
@@ -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.1.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
@@ -47,9 +33,10 @@ module ActionReporter
47
33
  PluginDiscovery.register(name, class_name: class_name, require_path: require_path)
48
34
  end
49
35
 
50
- @enabled_reporters = []
36
+ @enabled_reporters = nil
51
37
  @logger = nil
52
38
  @error_handler = nil
39
+ @user_id_resolver = nil
53
40
 
54
41
  def enabled_reporters=(reporters)
55
42
  @enabled_reporters = reporters || []
@@ -75,6 +62,18 @@ module ActionReporter
75
62
  @error_handler = handler
76
63
  end
77
64
 
65
+ # Optional proc to turn +current_user+ into a string id for reporters (Sentry, Scout, etc.).
66
+ # Receives the same object passed to +ActionReporter.current_user=+ and must return a String
67
+ # (or +nil+ to skip setting an id, if the reporter supports it).
68
+ # When unset, {ActionReporter::Base#resolve_user_id} uses built-in defaults.
69
+ def user_id_resolver
70
+ @user_id_resolver
71
+ end
72
+
73
+ def user_id_resolver=(resolver)
74
+ @user_id_resolver = resolver
75
+ end
76
+
78
77
  def handle_reporter_error(reporter, error, method_name)
79
78
  error_message = "ActionReporter: #{reporter.class}##{method_name} failed: #{error.class} - #{error.message}"
80
79
 
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.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov