action_reporter 1.5.2 → 2.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: dcc6906148e31a639c208838e67c4e975bc960184bafa53f22b197f20636259a
4
- data.tar.gz: 31887dcc92b62824aa84726f78419587f0e58628835c81c1854f99f80e365a55
3
+ metadata.gz: 966d8f43c69d090e7c05c60571b9514f70a78dc1f16406c50610c9003e02a9e5
4
+ data.tar.gz: b04f3e1ecc0a98d68b58169c25df6ad0a3ebbd23bc486b090cf8706b4e6b5b35
5
5
  SHA512:
6
- metadata.gz: 6a2dd2af3b8f111577787b50091bcb3153903b20190a764ab4ba00552071395b8de6b2f1c7bf9a1affacaa41ce9da2d2c9dce37c9edd641b03023eec42e39b4a
7
- data.tar.gz: 34090de9bb4cb9ccbda7540a60f1a947f58dcf724e2b18bfb70f30795e2288a23144c90fcd846decbfa041ddac490fd731a2eb29367cc11d5e984194b087b15e
6
+ metadata.gz: 6ccd9608cf52f7eb887102e12011b3f889cb7d06c171ddc46aa06c0a7ac69d9330dd7a377b12da3c3b17e71254bd940fe1f69f9f590401fcc3e02c6a29129f9f
7
+ data.tar.gz: 4f389d9305525cd87a2eb9b0b8f09765867e2195060c88b6fd942199bfcd5f4e118f9b896011b027f62cf55ed6a7965641455226ad479c0dbf32d48c457771b2
data/CHANGELOG.md CHANGED
@@ -1,77 +1,119 @@
1
- # 1.5.2
1
+ # CHANGELOG
2
2
 
3
- * Add Audited context support
3
+ ## 2.0.0 (2025-11-05)
4
4
 
5
- # 1.5.1
5
+ - Add performance benchmarks to measure CPU, memory, and response-time impact
6
+ - Benchmarks wall-time overhead for context + notify + reset operations
7
+ - Tests with 0 to 10 reporters to show scaling behavior
8
+ - Shows overhead per reporter (~1.5ms per reporter on average)
9
+ - Helps estimate response-time impact based on number of enabled reporters
10
+ - BREAKING: Complete rewrite of plugin discovery system
11
+ - New lazy-loaded plugin discovery that auto-discovers reporters from filesystem
12
+ - Introduced `ActionReporter::PluginDiscovery` module for managing reporter discovery
13
+ - `AVAILABLE_REPORTERS` constant maintained for backward compatibility but `available_reporters` method is now preferred
14
+ - Plugin discovery does not block application boot - all discovery is lazy-loaded
15
+ - BREAKING: Thread safety changes - context attributes are now thread-safe
16
+ - Replace module-level instance variables with thread-local storage using `ActionReporter::Current`
17
+ - Context attributes (`current_user`, `current_request_uuid`, `current_remote_addr`) are now thread-safe
18
+ - Prevents data leakage and race conditions in multi-threaded environments
19
+ - Maintains API compatibility but behavior is now thread-safe
20
+ - Add custom reporter registration support
21
+ - New `ActionReporter.register_reporter(name, class_name:, require_path:)` method
22
+ - Allows third-party gems and applications to register custom reporters (e.g., Datadog, New Relic)
23
+ - Registered reporters automatically appear in `available_reporters`
24
+ - Supports both file-based and inline-defined reporters
25
+ - Add `ActionReporter.available_reporters` method to get all available reporters (discovered + registered)
26
+ - Add thread-safe plugin discovery with Mutex-based caching
27
+ - Add comprehensive error handling to all reporter methods
28
+ - Reporter failures no longer break the entire reporting chain
29
+ - Errors are logged but don't prevent other reporters from executing
30
+ - Added `ActionReporter.logger` for configurable error logging (defaults to `Rails.logger` if available)
31
+ - Added `ActionReporter.error_handler` for custom error handling callbacks
32
+ - Fix `reset_context` method to properly reset instance attributes (`@current_user`, `@current_request_uuid`, `@current_remote_addr`)
33
+ - Improve error handling in plugin discovery - gracefully handles missing files, invalid classes, and load errors
34
+ - Add `ActionReporter::PluginDiscovery.reset!` method for testing purposes
35
+ - Add thread safety tests
36
+ - Add error handling tests with fault isolation verification
37
+ - Add comprehensive plugin discovery tests
38
+ - Add custom reporter registration tests
39
+ - Achieve 100% test coverage (297/297 lines)
6
40
 
7
- * Fix Audited current user setter to use `audited_user`
41
+ ## 1.5.2 (2024-12-13)
8
42
 
9
- # 1.5.0
43
+ - Add Audited context support
10
44
 
11
- * BREAKING: Rename `audited_user` to `current_user`
12
- * Add `current_request_uuid` and `current_remote_addr` getters and setters
13
- * Memoize `current_user`, `current_request_uuid`, and `current_remote_addr`
45
+ ## 1.5.1 (2024-10-29)
14
46
 
15
- # 1.4.1
47
+ - Fix Audited current user setter to use `audited_user`
16
48
 
17
- * Add paper_trail support
49
+ ## 1.5.0 (2024-10-29)
18
50
 
19
- # 1.4.0
51
+ - BREAKING: Rename `audited_user` to `current_user`
52
+ - Add `current_request_uuid` and `current_remote_addr` getters and setters
53
+ - Memoize `current_user`, `current_request_uuid`, and `current_remote_addr`
20
54
 
21
- * Set minimum ruby version requirement to 2.5.0
55
+ ## 1.4.1 (2024-02-02)
22
56
 
23
- # 1.3.1
57
+ - Add paper_trail support
24
58
 
25
- * Update gem configuration
59
+ ## 1.4.0 (2023-08-30)
26
60
 
27
- # 1.3.0
61
+ - Set minimum ruby version requirement to 2.5.0
28
62
 
29
- * Update ruby version to 3.2.2
30
- * Update dependencies to latest
63
+ ## 1.3.2 (2023-08-29)
31
64
 
32
- # 1.2.0
65
+ ## 1.3.1 (2023-08-29)
33
66
 
34
- * Major fixes for class resolvers
35
- * Implemented ActionReporter::Error class
36
- * Improved test coverage
67
+ - Update gem configuration
37
68
 
38
- # 1.1.1
69
+ ## 1.3.0 (2023-08-15)
39
70
 
40
- * Update check-in logic
71
+ - Update ruby version to 3.2.2
72
+ - Update dependencies to latest
41
73
 
42
- # 1.1.0
74
+ ## 1.2.0 (2023-04-20)
43
75
 
44
- * Add reporter check-in method
76
+ - Major fixes for class resolvers
77
+ - Implemented ActionReporter::Error class
78
+ - Improved test coverage
45
79
 
46
- # 1.0.7
80
+ ## 1.1.1 (2023-04-18)
47
81
 
48
- * Moving Sentry context under `context` key
82
+ - Update check-in logic
49
83
 
50
- # 1.0.6
84
+ ## 1.1.0 (2023-04-18)
51
85
 
52
- * Possible fix for Sentry context setting
86
+ - Add reporter check-in method
53
87
 
54
- # 1.0.5
88
+ ## 1.0.7 (2023-04-18)
55
89
 
56
- * Fix Sentry reporting and context setting
90
+ - Moving Sentry context under `context` key
57
91
 
58
- # 1.0.4
92
+ ## 1.0.6 (2023-04-18)
59
93
 
60
- * Move `transform_context` to individual reporter classes
94
+ - Possible fix for Sentry context setting
61
95
 
62
- # 1.0.3
96
+ ## 1.0.5 (2023-04-18)
63
97
 
64
- * Fix scoutapm notice method
98
+ - Fix Sentry reporting and context setting
65
99
 
66
- # 1.0.2
100
+ ## 1.0.4 (2023-04-18)
67
101
 
68
- * Add ruby version support for versions lower than 3.2.0
102
+ - Move `transform_context` to individual reporter classes
69
103
 
70
- # 1.0.1
104
+ ## 1.0.3 (2023-04-18)
71
105
 
72
- * Fix scoutapm reset_context method
73
- * Update README notes
106
+ - Fix scoutapm notice method
74
107
 
75
- # 1.0.0
108
+ ## 1.0.2 (2023-04-18)
76
109
 
77
- * Initial version
110
+ - Add ruby version support for versions lower than 3.2.0
111
+
112
+ ## 1.0.1 (2023-04-18)
113
+
114
+ - Fix scoutapm reset_context method
115
+ - Update README notes
116
+
117
+ ## 1.0.0 (2023-04-18)
118
+
119
+ - Initial version
data/README.md CHANGED
@@ -2,89 +2,163 @@
2
2
 
3
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)
4
4
 
5
- [![Codacy Badge](https://app.codacy.com/project/badge/Grade/f4bef9a52eac43a5a0f6d8c1b58cc6af)](https://app.codacy.com/gh/amkisko/action_reporter.rb/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/f4bef9a52eac43a5a0f6d8c1b58cc6af)](https://app.codacy.com/gh/amkisko/action_reporter.rb/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
6
-
7
5
  Ruby wrapper for multiple reporting services.
8
6
 
9
- Supported services:
10
- - Rails logger
11
- - Audited
12
- - PaperTrail
13
- - Sentry
14
- - Honeybadger
15
- - scoutapm
7
+ Supported services: Rails logger, gem audited, gem PaperTrail, Sentry, Honeybadger, scoutapm.
16
8
 
17
9
  Sponsored by [Kisko Labs](https://www.kiskolabs.com).
18
10
 
19
- ## Install
11
+ <a href="https://www.kiskolabs.com">
12
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
13
+ </a>
20
14
 
21
- Using Bundler:
22
- ```sh
23
- bundle add action_reporter
24
- ```
25
15
 
26
- Using RubyGems:
27
- ```sh
28
- gem install action_reporter
29
- ```
16
+ ## Installation
30
17
 
31
- ## Gemfile
18
+ Add to your Gemfile:
32
19
 
33
20
  ```ruby
34
- gem 'action_reporter'
21
+ gem "action_reporter"
35
22
  ```
36
23
 
24
+ Run `bundle install` or `gem install action_reporter`.
25
+
26
+
37
27
  ## Usage
38
28
 
39
- Put this in your `config/initializers/action_reporter.rb` file:
29
+ Create `config/initializers/action_reporter.rb`:
40
30
 
41
31
  ```ruby
42
32
  ActionReporter.enabled_reporters = [
43
- ActionReporter::RailsReporter.new,
33
+ (ActionReporter::RailsReporter.new if Rails.env.development?),
44
34
  # ActionReporter::AuditedReporter.new,
45
35
  # ActionReporter::PaperTrailReporter.new,
46
36
  # ActionReporter::SentryReporter.new,
47
37
  # ActionReporter::HoneybadgerReporter.new,
48
38
  # ActionReporter::ScoutApmReporter.new
49
- ]
39
+ ].compact
50
40
  ```
51
41
 
52
- Then you can use it in your code:
42
+ Set context and report errors:
53
43
 
54
44
  ```ruby
55
45
  ActionReporter.current_user = current_user
56
- ActionReporter.current_request_uuid = request.env['action_dispatch.request_id']
57
- ActionReporter.current_remote_addr = request.env['action_dispatch.remote_ip']
46
+ ActionReporter.current_request_uuid = request.env["action_dispatch.request_id"]
47
+ ActionReporter.current_remote_addr = request.remote_ip
48
+
58
49
  ActionReporter.context(entry_id: entry.id)
59
- ActionReporter.notify('Something went wrong', context: { record: record })
50
+ ActionReporter.notify("Something went wrong", context: { record: record })
51
+ ActionReporter.reset_context
52
+ ```
53
+
54
+ ## Transaction support
55
+
56
+ ActionReporter supports transaction tracking with automatic context preservation:
57
+
58
+ ```ruby
59
+ # Attribute-style setters
60
+ ActionReporter.transaction_id = "txn-123"
61
+ ActionReporter.transaction_name = "GET /api/users"
62
+
63
+ # Block-based (preserves previous values)
64
+ ActionReporter.transaction(name: "GET /api/users", id: "txn-123") do
65
+ # Your code here
66
+ end
60
67
  ```
61
68
 
62
- ## Hook debugger to notify method
69
+ ## Custom Reporters
63
70
 
64
- Apply patch on initializer level or before running the main code:
71
+ Create custom reporters by inheriting from `ActionReporter::Base`:
65
72
 
66
73
  ```ruby
67
74
  module ActionReporter
68
- class RailsReporter < Base
75
+ class CustomReporter < Base
69
76
  def notify(error, context: {})
70
- super
71
- binding.pry
77
+ new_context = transform_context(context)
78
+ # Send to your service
79
+ end
80
+
81
+ def context(args)
82
+ new_context = transform_context(args)
83
+ # Set context in your service
72
84
  end
73
85
  end
74
86
  end
87
+
88
+ ActionReporter.enabled_reporters = [ActionReporter::CustomReporter.new]
75
89
  ```
76
90
 
91
+ See `doc/CUSTOM_REPORTERS.md` for detailed documentation.
92
+
93
+ ## Advanced Integration
94
+
95
+ ActionReporter can be extended with custom methods and integrated with `ActiveSupport::CurrentAttributes` for automatic context propagation:
96
+
97
+ ```ruby
98
+ module ActionReporter
99
+ def self.set_transaction_id(transaction_id)
100
+ context(transaction_id: transaction_id)
101
+ Sentry.set_tags(transactionId: transaction_id) if defined?(Sentry)
102
+ end
103
+ end
104
+
105
+ class Current < ActiveSupport::CurrentAttributes
106
+ attribute :user, :reporter_transaction_id
107
+
108
+ def user=(user)
109
+ super
110
+ ActionReporter.current_user = user
111
+ end
112
+
113
+ def reporter_transaction_id=(transaction_id)
114
+ super
115
+ ActionReporter.transaction_id = transaction_id
116
+ end
117
+ end
118
+ ```
119
+
120
+ ## API
121
+
122
+ - `ActionReporter.enabled_reporters = [...]` - Configure enabled reporters
123
+ - `ActionReporter.current_user = user` - Set current user (thread-safe)
124
+ - `ActionReporter.current_request_uuid = uuid` - Set request UUID
125
+ - `ActionReporter.current_remote_addr = addr` - Set remote address
126
+ - `ActionReporter.context(**args)` - Set context for all reporters
127
+ - `ActionReporter.notify(error, context: {})` - Report errors/messages
128
+ - `ActionReporter.reset_context` - Reset all context
129
+ - `ActionReporter.transaction_id = id` - Set transaction ID
130
+ - `ActionReporter.transaction_name = name` - Set transaction name
131
+ - `ActionReporter.transaction(name:, id:, **context, &block)` - Block-based transaction with context preservation
132
+ - `ActionReporter.check_in(identifier)` - Heartbeat/check-in
133
+ - `ActionReporter.logger = logger` - Configure error logger
134
+ - `ActionReporter.error_handler = proc` - Configure error handler callback
135
+
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ bundle install
141
+ bundle exec appraisal install
142
+ bundle exec rspec
143
+ bin/appraisals
144
+ bundle exec standardrb --fix
145
+ ```
146
+
147
+
77
148
  ## Contributing
78
149
 
79
150
  Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/action_reporter.rb
80
151
 
81
152
  Contribution policy:
153
+ - New features are not necessarily added to the gem
154
+ - Pull request should have test coverage for affected parts
155
+ - Pull request should have changelog entry
156
+
157
+ Review policy:
82
158
  - It might take up to 2 calendar weeks to review and merge critical fixes
83
159
  - It might take up to 6 calendar months to review and merge pull request
84
160
  - It might take up to 1 calendar year to review an issue
85
- - New integrations and third-party features are not nessessarily added to the gem
86
- - Pull request should have test coverage for affected parts
87
- - Pull request should have changelog entry
161
+
88
162
 
89
163
  ## Publishing
90
164
 
@@ -94,6 +168,7 @@ gem build action_reporter.gemspec
94
168
  gem push action_reporter-*.gem
95
169
  ```
96
170
 
171
+
97
172
  ## License
98
173
 
99
174
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,6 +1,8 @@
1
+ require_relative "lib/action_reporter/version"
2
+
1
3
  Gem::Specification.new do |gem|
2
4
  gem.name = "action_reporter"
3
- gem.version = File.read(File.expand_path("../lib/action_reporter.rb", __FILE__)).match(/VERSION\s*=\s*'(.*?)'/)[1]
5
+ gem.version = ActionReporter::VERSION
4
6
 
5
7
  gem.license = "MIT"
6
8
 
@@ -9,7 +11,7 @@ Gem::Specification.new do |gem|
9
11
  repository_url = "https://github.com/amkisko/action_reporter.rb"
10
12
 
11
13
  gem.authors = ["Andrei Makarov"]
12
- gem.email = ["andrei@kiskolabs.com"]
14
+ gem.email = ["contact@kiskolabs.com"]
13
15
  gem.homepage = repository_url
14
16
  gem.description = "Ruby wrapper for multiple reporting services"
15
17
  gem.summary = "See description"
@@ -37,10 +39,14 @@ Gem::Specification.new do |gem|
37
39
  gem.add_development_dependency "scout_apm", "~> 5"
38
40
  gem.add_development_dependency "paper_trail", "~> 15"
39
41
 
40
- gem.add_development_dependency "rspec", "~> 3"
42
+ gem.add_development_dependency "bundler", "~> 2"
43
+ gem.add_development_dependency "rspec", "~> 3.12"
41
44
  gem.add_development_dependency "rspec_junit_formatter", "~> 0.6"
42
45
  gem.add_development_dependency "webmock", "~> 3"
43
46
  gem.add_development_dependency "pry", "~> 0.14"
44
47
  gem.add_development_dependency "simplecov", "~> 0.21"
45
- gem.add_development_dependency "simplecov-cobertura", "~> 2"
48
+ gem.add_development_dependency "simplecov-cobertura", "~> 3"
49
+ gem.add_development_dependency "standard", "~> 1.0"
50
+ gem.add_development_dependency "appraisal", "~> 2.4"
51
+ gem.add_development_dependency "rbs", "~> 3.0"
46
52
  end
@@ -1,4 +1,4 @@
1
- require_relative 'error'
1
+ require_relative "error"
2
2
 
3
3
  module ActionReporter
4
4
  class Base
@@ -7,13 +7,15 @@ module ActionReporter
7
7
  define_method(method_name) do
8
8
  raise ActionReporter::Error.new("#{class_name} is not defined") unless Object.const_defined?(class_name)
9
9
 
10
- @@class_cache ||= {}
11
- @@class_cache[class_name] ||= begin
10
+ # Use instance variable instead of class variable for thread safety
11
+ # Each class gets its own cache, avoiding cross-class contamination
12
+ @class_cache ||= {}
13
+ @class_cache[class_name] ||= begin
12
14
  if gem_spec
13
15
  gem_name, version = gem_spec.scan(/([^(\s]+)\s*(?:\(([^)]+)\))?/).first
14
16
  latest_spec = Gem.loaded_specs[gem_name]
15
17
  version_satisfied = latest_spec && Gem::Requirement.new(version).satisfied_by?(latest_spec.version)
16
- raise ActionReporter::Error.new("#{gem_spec} is not loaded") if !version_satisfied
18
+ raise ActionReporter::ConfigurationError.new("#{gem_spec} is not loaded") if !version_satisfied
17
19
  end
18
20
  Object.const_get(class_name)
19
21
  end
@@ -36,7 +38,7 @@ module ActionReporter
36
38
  elsif identifier.respond_to?(:to_s)
37
39
  identifier.to_s
38
40
  else
39
- raise ArgumentError.new("Unknown check-in identifier: #{identifier.inspect}")
41
+ raise ActionReporter::Error.new("Unknown check-in identifier: #{identifier.inspect}")
40
42
  end
41
43
  end
42
44
 
@@ -48,5 +50,11 @@ module ActionReporter
48
50
 
49
51
  def reset_context
50
52
  end
53
+
54
+ def transaction_id=(transaction_id)
55
+ end
56
+
57
+ def transaction_name=(transaction_name)
58
+ end
51
59
  end
52
60
  end
@@ -0,0 +1,59 @@
1
+ require_relative "error"
2
+
3
+ module ActionReporter
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
8
+ class Current
9
+ class << self
10
+ def current_user
11
+ Thread.current[:action_reporter_current_user]
12
+ end
13
+
14
+ def current_user=(user)
15
+ Thread.current[:action_reporter_current_user] = user
16
+ end
17
+
18
+ def current_request_uuid
19
+ Thread.current[:action_reporter_current_request_uuid]
20
+ end
21
+
22
+ def current_request_uuid=(uuid)
23
+ Thread.current[:action_reporter_current_request_uuid] = uuid
24
+ end
25
+
26
+ def current_remote_addr
27
+ Thread.current[:action_reporter_current_remote_addr]
28
+ end
29
+
30
+ def current_remote_addr=(addr)
31
+ Thread.current[:action_reporter_current_remote_addr] = addr
32
+ end
33
+
34
+ def transaction_id
35
+ Thread.current[:action_reporter_transaction_id]
36
+ end
37
+
38
+ def transaction_id=(transaction_id)
39
+ Thread.current[:action_reporter_transaction_id] = transaction_id
40
+ end
41
+
42
+ def transaction_name
43
+ Thread.current[:action_reporter_transaction_name]
44
+ end
45
+
46
+ def transaction_name=(transaction_name)
47
+ Thread.current[:action_reporter_transaction_name] = transaction_name
48
+ end
49
+
50
+ 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
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,9 @@
1
1
  module ActionReporter
2
2
  class Error < StandardError; end
3
+
4
+ # Raised when there's a configuration issue (e.g., missing gem, invalid setup)
5
+ class ConfigurationError < Error; end
6
+
7
+ # Raised when a reporter encounters an error during operation
8
+ class ReporterError < Error; end
3
9
  end
@@ -0,0 +1,143 @@
1
+ require_relative "base"
2
+
3
+ module ActionReporter
4
+ # Plugin discovery mechanism for auto-discovering reporters
5
+ # This is lazy-loaded to avoid blocking application boot
6
+ module PluginDiscovery
7
+ class << self
8
+ # Initialize class instance variables
9
+ @registered_reporters = {}
10
+ @discovered_reporters = nil
11
+ @discovery_lock = Mutex.new
12
+
13
+ # Accessors for class instance variables
14
+ def registered_reporters
15
+ @registered_reporters ||= {}
16
+ end
17
+
18
+ attr_reader :discovered_reporters
19
+
20
+ attr_writer :discovered_reporters
21
+
22
+ def discovery_lock
23
+ @discovery_lock ||= Mutex.new
24
+ end
25
+
26
+ # Register a reporter manually (useful for custom reporters)
27
+ # @param name [Symbol] Reporter name
28
+ # @param class_name [String] Fully qualified class name
29
+ # @param require_path [String] Path to require (e.g., "action_reporter/custom_reporter")
30
+ def register(name, class_name:, require_path:)
31
+ registered_reporters[name] = {
32
+ class_name: class_name,
33
+ require_path: require_path
34
+ }
35
+ end
36
+
37
+ # Discover reporters from the filesystem (lazy-loaded, cached)
38
+ # Only discovers files matching *_reporter.rb pattern in action_reporter/ directory
39
+ # @return [Array<Class>] Array of reporter classes
40
+ def discover
41
+ return discovered_reporters if discovered_reporters
42
+
43
+ discovery_lock.synchronize do
44
+ return discovered_reporters if discovered_reporters
45
+
46
+ self.discovered_reporters = []
47
+ # __dir__ is lib/action_reporter/, so we're already in the right directory
48
+ base_path = __dir__
49
+
50
+ # Discover built-in reporters
51
+ Dir.glob(File.join(base_path, "*_reporter.rb")).each do |file|
52
+ reporter_class = discover_reporter_from_file(file)
53
+ discovered_reporters << reporter_class if reporter_class
54
+ rescue => e
55
+ # Silently skip files that can't be loaded (non-blocking)
56
+ warn "ActionReporter: Failed to discover reporter from #{file}: #{e.message}" if logger
57
+ end
58
+
59
+ self.discovered_reporters = discovered_reporters.freeze
60
+ end
61
+ end
62
+
63
+ # Get all available reporters (discovered + registered)
64
+ # @return [Array<Class>] Array of reporter classes
65
+ def available_reporters
66
+ reporters = discover.dup
67
+
68
+ # Add registered reporters (lazy-loaded)
69
+ registered_reporters.each_value do |config|
70
+ reporter_class = load_registered_reporter(config)
71
+ reporters << reporter_class if reporter_class
72
+ rescue => e
73
+ # Silently skip registered reporters that can't be loaded (non-blocking)
74
+ warn "ActionReporter: Failed to load registered reporter #{config[:class_name]}: #{e.message}" if logger
75
+ end
76
+
77
+ reporters.uniq.freeze
78
+ end
79
+
80
+ # Reset discovery cache (useful for testing)
81
+ def reset!
82
+ discovery_lock.synchronize do
83
+ self.discovered_reporters = nil
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def discover_reporter_from_file(file_path)
90
+ base_name = File.basename(file_path, ".rb")
91
+
92
+ # Convert snake_case to PascalCase
93
+ # e.g., "scout_apm_reporter" -> "ScoutApmReporter"
94
+ # e.g., "rails_reporter" -> "RailsReporter"
95
+ class_name = base_name
96
+ .split("_")
97
+ .map(&:capitalize)
98
+ .join
99
+
100
+ full_class_name = "ActionReporter::#{class_name}"
101
+
102
+ # Check if class is already defined (files are pre-required at boot)
103
+ return nil unless Object.const_defined?(full_class_name)
104
+
105
+ klass = Object.const_get(full_class_name)
106
+ return nil unless klass < Base
107
+
108
+ klass
109
+ end
110
+
111
+ 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])
121
+ begin
122
+ require config[:require_path]
123
+ 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
+ warn "ActionReporter: Could not require #{config[:require_path]}: #{e.message}" if logger
127
+ # Continue - class might already be defined
128
+ end
129
+ end
130
+
131
+ klass
132
+ end
133
+
134
+ def required?(path)
135
+ $LOADED_FEATURES.any? { |feature| feature.include?(path) }
136
+ end
137
+
138
+ def logger
139
+ ActionReporter.logger
140
+ end
141
+ end
142
+ end
143
+ end
@@ -25,5 +25,15 @@ module ActionReporter
25
25
  def current_user=(user)
26
26
  sentry_class.set_user(user_global_id: user&.to_global_id&.to_s)
27
27
  end
28
+
29
+ def transaction_id=(transaction_id)
30
+ sentry_class.set_tags(transactionId: transaction_id)
31
+ end
32
+
33
+ def transaction_name=(transaction_name)
34
+ sentry_class.configure_scope do |scope|
35
+ scope.set_transaction_name(transaction_name)
36
+ end
37
+ end
28
38
  end
29
39
  end
@@ -0,0 +1,3 @@
1
+ module ActionReporter
2
+ VERSION = "2.0.0"
3
+ end
@@ -1,17 +1,23 @@
1
- require 'action_reporter/utils'
2
- require 'action_reporter/base'
3
- require 'action_reporter/rails_reporter'
4
- require 'action_reporter/honeybadger_reporter'
5
- require 'action_reporter/sentry_reporter'
6
- require 'action_reporter/scout_apm_reporter'
7
- require 'action_reporter/audited_reporter'
8
- require 'action_reporter/paper_trail_reporter'
1
+ require "action_reporter/version"
2
+ require "action_reporter/utils"
3
+ require "action_reporter/base"
4
+ require "action_reporter/current"
5
+ require "action_reporter/plugin_discovery"
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"
9
15
 
10
16
  module ActionReporter
11
17
  module_function
12
18
 
13
- VERSION = '1.5.2'.freeze
14
-
19
+ # Legacy hardcoded list (maintained for backward compatibility)
20
+ # Use `available_reporters` for auto-discovered reporters
15
21
  AVAILABLE_REPORTERS = [
16
22
  ActionReporter::RailsReporter,
17
23
  ActionReporter::HoneybadgerReporter,
@@ -21,76 +27,163 @@ module ActionReporter
21
27
  ActionReporter::PaperTrailReporter
22
28
  ].freeze
23
29
 
30
+ # Get available reporters (auto-discovered + registered)
31
+ # This is lazy-loaded and does not block application boot
32
+ # @return [Array<Class>] Array of reporter classes
33
+ def available_reporters
34
+ PluginDiscovery.available_reporters
35
+ end
36
+
37
+ # Register a custom reporter
38
+ # This allows third-party gems or custom code to register reporters
39
+ # @param name [Symbol] Reporter name
40
+ # @param class_name [String] Fully qualified class name
41
+ # @param require_path [String] Path to require
42
+ # @example
43
+ # ActionReporter.register_reporter(:custom, class_name: "MyApp::CustomReporter", require_path: "my_app/custom_reporter")
44
+ def register_reporter(name, class_name:, require_path:)
45
+ PluginDiscovery.register(name, class_name: class_name, require_path: require_path)
46
+ end
47
+
24
48
  @enabled_reporters = []
49
+ @logger = nil
50
+ @error_handler = nil
25
51
 
26
52
  def enabled_reporters=(reporters)
27
- @enabled_reporters = reporters
53
+ @enabled_reporters = reporters || []
28
54
  end
29
55
 
30
56
  def enabled_reporters
31
- @enabled_reporters
57
+ @enabled_reporters || []
58
+ end
59
+
60
+ def logger
61
+ @logger || ((defined?(Rails) && Rails.respond_to?(:logger)) ? Rails.logger : nil)
62
+ end
63
+
64
+ def logger=(logger)
65
+ @logger = logger
66
+ end
67
+
68
+ def error_handler
69
+ @error_handler
70
+ end
71
+
72
+ def error_handler=(handler)
73
+ @error_handler = handler
74
+ end
75
+
76
+ def handle_reporter_error(reporter, error, method_name)
77
+ error_message = "ActionReporter: #{reporter.class}##{method_name} failed: #{error.class} - #{error.message}"
78
+
79
+ if logger
80
+ logger.error(error_message)
81
+ logger.debug(error.backtrace.join("\n")) if error.backtrace
82
+ end
83
+
84
+ if error_handler&.respond_to?(:call)
85
+ error_handler.call(error, reporter, method_name)
86
+ end
87
+ rescue => e
88
+ # If error handling itself fails, log to stderr as last resort
89
+ warn "ActionReporter: Error handler failed: #{e.message}"
32
90
  end
33
91
 
34
92
  def notify(error, context: {})
35
93
  enabled_reporters.each do |reporter|
36
94
  next unless reporter.respond_to?(:notify)
37
95
 
38
- reporter.notify(error, context: context)
96
+ begin
97
+ reporter.notify(error, context: context)
98
+ rescue => e
99
+ handle_reporter_error(reporter, e, "notify")
100
+ end
39
101
  end
40
102
  end
41
103
 
42
104
  def context(args)
105
+ raise ArgumentError, "context must be a Hash" unless args.is_a?(Hash)
106
+
43
107
  enabled_reporters.each do |reporter|
44
108
  next unless reporter.respond_to?(:context)
45
109
 
46
- reporter.context(args)
110
+ begin
111
+ reporter.context(args)
112
+ rescue => e
113
+ handle_reporter_error(reporter, e, "context")
114
+ end
47
115
  end
48
116
  end
49
117
 
50
118
  def reset_context
119
+ Current.current_user = nil
120
+ Current.current_request_uuid = nil
121
+ Current.current_remote_addr = nil
122
+ Current.transaction_id = nil
123
+ Current.transaction_name = nil
124
+
51
125
  enabled_reporters.each do |reporter|
52
126
  next unless reporter.respond_to?(:reset_context)
53
127
 
54
- reporter.reset_context
128
+ begin
129
+ reporter.reset_context
130
+ rescue => e
131
+ handle_reporter_error(reporter, e, "reset_context")
132
+ end
55
133
  end
134
+
135
+ # Reset Current attributes if supported
136
+ Current.reset if Current.respond_to?(:reset)
56
137
  end
57
138
 
58
139
  def current_user
59
- @current_user
140
+ Current.current_user
60
141
  end
61
142
 
62
143
  def current_user=(user)
63
- @current_user = user
144
+ Current.current_user = user
64
145
  enabled_reporters.each do |reporter|
65
146
  next unless reporter.respond_to?(:current_user=)
66
147
 
67
- reporter.current_user = user
148
+ begin
149
+ reporter.current_user = user
150
+ rescue => e
151
+ handle_reporter_error(reporter, e, "current_user=")
152
+ end
68
153
  end
69
154
  end
70
155
 
71
156
  def current_request_uuid
72
- @current_request_uuid
157
+ Current.current_request_uuid
73
158
  end
74
159
 
75
160
  def current_request_uuid=(request_uuid)
76
- @current_request_uuid = request_uuid
161
+ Current.current_request_uuid = request_uuid
77
162
  enabled_reporters.each do |reporter|
78
163
  next unless reporter.respond_to?(:current_request_uuid=)
79
164
 
80
- reporter.current_request_uuid = request_uuid
165
+ begin
166
+ reporter.current_request_uuid = request_uuid
167
+ rescue => e
168
+ handle_reporter_error(reporter, e, "current_request_uuid=")
169
+ end
81
170
  end
82
171
  end
83
172
 
84
173
  def current_remote_addr
85
- @current_remote_addr
174
+ Current.current_remote_addr
86
175
  end
87
176
 
88
177
  def current_remote_addr=(remote_addr)
89
- @current_remote_addr = remote_addr
178
+ Current.current_remote_addr = remote_addr
90
179
  enabled_reporters.each do |reporter|
91
180
  next unless reporter.respond_to?(:current_remote_addr=)
92
181
 
93
- reporter.current_remote_addr = remote_addr
182
+ begin
183
+ reporter.current_remote_addr = remote_addr
184
+ rescue => e
185
+ handle_reporter_error(reporter, e, "current_remote_addr=")
186
+ end
94
187
  end
95
188
  end
96
189
 
@@ -98,7 +191,68 @@ module ActionReporter
98
191
  enabled_reporters.each do |reporter|
99
192
  next unless reporter.respond_to?(:check_in)
100
193
 
101
- reporter.check_in(identifier)
194
+ begin
195
+ reporter.check_in(identifier)
196
+ rescue => e
197
+ handle_reporter_error(reporter, e, "check_in")
198
+ end
199
+ end
200
+ end
201
+
202
+ def transaction_id
203
+ Current.transaction_id
204
+ end
205
+
206
+ def transaction_id=(transaction_id)
207
+ Current.transaction_id = transaction_id
208
+ context(transaction_id: transaction_id)
209
+
210
+ enabled_reporters.each do |reporter|
211
+ next unless reporter.respond_to?(:transaction_id=)
212
+
213
+ begin
214
+ reporter.transaction_id = transaction_id
215
+ rescue => e
216
+ handle_reporter_error(reporter, e, "transaction_id=")
217
+ end
218
+ end
219
+ end
220
+
221
+ def transaction_name
222
+ Current.transaction_name
223
+ end
224
+
225
+ def transaction_name=(transaction_name)
226
+ Current.transaction_name = transaction_name
227
+ context(transaction_name: transaction_name)
228
+
229
+ enabled_reporters.each do |reporter|
230
+ next unless reporter.respond_to?(:transaction_name=)
231
+
232
+ begin
233
+ reporter.transaction_name = transaction_name
234
+ rescue => e
235
+ handle_reporter_error(reporter, e, "transaction_name=")
236
+ end
237
+ end
238
+ end
239
+
240
+ def transaction(name: nil, id: nil, **context_options, &block)
241
+ raise ArgumentError, "transaction requires a block" unless block_given?
242
+
243
+ previous_name = transaction_name
244
+ previous_id = transaction_id
245
+
246
+ begin
247
+ self.transaction_name = name if name
248
+ self.transaction_id = id if id
249
+
250
+ context(context_options) if context_options.any?
251
+
252
+ block.call
253
+ ensure
254
+ self.transaction_name = previous_name if name
255
+ self.transaction_id = previous_id if id
102
256
  end
103
257
  end
104
258
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-13 00:00:00.000000000 Z
11
+ date: 2025-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -94,20 +94,34 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '15'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rspec
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '3'
117
+ version: '3.12'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '3'
124
+ version: '3.12'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rspec_junit_formatter
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -170,17 +184,59 @@ dependencies:
170
184
  requirements:
171
185
  - - "~>"
172
186
  - !ruby/object:Gem::Version
173
- version: '2'
187
+ version: '3'
174
188
  type: :development
175
189
  prerelease: false
176
190
  version_requirements: !ruby/object:Gem::Requirement
177
191
  requirements:
178
192
  - - "~>"
179
193
  - !ruby/object:Gem::Version
180
- version: '2'
194
+ version: '3'
195
+ - !ruby/object:Gem::Dependency
196
+ name: standard
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '1.0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '1.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: appraisal
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '2.4'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '2.4'
223
+ - !ruby/object:Gem::Dependency
224
+ name: rbs
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '3.0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: '3.0'
181
237
  description: Ruby wrapper for multiple reporting services
182
238
  email:
183
- - andrei@kiskolabs.com
239
+ - contact@kiskolabs.com
184
240
  executables: []
185
241
  extensions: []
186
242
  extra_rdoc_files: []
@@ -192,13 +248,16 @@ files:
192
248
  - lib/action_reporter.rb
193
249
  - lib/action_reporter/audited_reporter.rb
194
250
  - lib/action_reporter/base.rb
251
+ - lib/action_reporter/current.rb
195
252
  - lib/action_reporter/error.rb
196
253
  - lib/action_reporter/honeybadger_reporter.rb
197
254
  - lib/action_reporter/paper_trail_reporter.rb
255
+ - lib/action_reporter/plugin_discovery.rb
198
256
  - lib/action_reporter/rails_reporter.rb
199
257
  - lib/action_reporter/scout_apm_reporter.rb
200
258
  - lib/action_reporter/sentry_reporter.rb
201
259
  - lib/action_reporter/utils.rb
260
+ - lib/action_reporter/version.rb
202
261
  homepage: https://github.com/amkisko/action_reporter.rb
203
262
  licenses:
204
263
  - MIT