action_reporter 1.5.2 → 2.0.1

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: 60e2a8fa1070b5e5c83b3437da921488a850e8e7e753d35a0fee191c5f6b3134
4
+ data.tar.gz: dd157cad1416a343ac6231ab0af51a90861acb840471651642991e395009a7f4
5
5
  SHA512:
6
- metadata.gz: 6a2dd2af3b8f111577787b50091bcb3153903b20190a764ab4ba00552071395b8de6b2f1c7bf9a1affacaa41ce9da2d2c9dce37c9edd641b03023eec42e39b4a
7
- data.tar.gz: 34090de9bb4cb9ccbda7540a60f1a947f58dcf724e2b18bfb70f30795e2288a23144c90fcd846decbfa041ddac490fd731a2eb29367cc11d5e984194b087b15e
6
+ metadata.gz: 2b649899b117fc77b1ced1f5fa2979e88091fb9b3896ecdc1fccc87c8be59277727fc63c5a3da97d85b4066d09dc14bff91ae85f7a9ce77797caf9151174cff9
7
+ data.tar.gz: 4591b286098f6090e00484bbe1afe0145711d0d3a641a51ac6b80b335ccb29cd316537ac0ee7db3409352e1abbe6b942b8073d0aa96afb953f58d7112512ad8d
data/CHANGELOG.md CHANGED
@@ -1,77 +1,126 @@
1
- # 1.5.2
1
+ # CHANGELOG
2
2
 
3
- * Add Audited context support
3
+ ## 2.0.1 (2025-11-14)
4
4
 
5
- # 1.5.1
5
+ - Fix false positives in gem version validation for git-based gems
6
+ - Remove strict version requirement check that caused `ConfigurationError` for valid git-based gem installations
7
+ - Class existence check is sufficient to ensure gem is loaded
8
+ - Fixes issue where `sentry-ruby` and other git-based gems were incorrectly flagged as not loaded
6
9
 
7
- * Fix Audited current user setter to use `audited_user`
10
+ ## 2.0.0 (2025-11-05)
8
11
 
9
- # 1.5.0
12
+ - Add performance benchmarks to measure CPU, memory, and response-time impact
13
+ - Benchmarks wall-time overhead for context + notify + reset operations
14
+ - Tests with 0 to 10 reporters to show scaling behavior
15
+ - Shows overhead per reporter (~1.5ms per reporter on average)
16
+ - Helps estimate response-time impact based on number of enabled reporters
17
+ - BREAKING: Complete rewrite of plugin discovery system
18
+ - New lazy-loaded plugin discovery that auto-discovers reporters from filesystem
19
+ - Introduced `ActionReporter::PluginDiscovery` module for managing reporter discovery
20
+ - `AVAILABLE_REPORTERS` constant maintained for backward compatibility but `available_reporters` method is now preferred
21
+ - Plugin discovery does not block application boot - all discovery is lazy-loaded
22
+ - BREAKING: Thread safety changes - context attributes are now thread-safe
23
+ - Replace module-level instance variables with thread-local storage using `ActionReporter::Current`
24
+ - Context attributes (`current_user`, `current_request_uuid`, `current_remote_addr`) are now thread-safe
25
+ - Prevents data leakage and race conditions in multi-threaded environments
26
+ - Maintains API compatibility but behavior is now thread-safe
27
+ - Add custom reporter registration support
28
+ - New `ActionReporter.register_reporter(name, class_name:, require_path:)` method
29
+ - Allows third-party gems and applications to register custom reporters (e.g., Datadog, New Relic)
30
+ - Registered reporters automatically appear in `available_reporters`
31
+ - Supports both file-based and inline-defined reporters
32
+ - Add `ActionReporter.available_reporters` method to get all available reporters (discovered + registered)
33
+ - Add thread-safe plugin discovery with Mutex-based caching
34
+ - Add comprehensive error handling to all reporter methods
35
+ - Reporter failures no longer break the entire reporting chain
36
+ - Errors are logged but don't prevent other reporters from executing
37
+ - Added `ActionReporter.logger` for configurable error logging (defaults to `Rails.logger` if available)
38
+ - Added `ActionReporter.error_handler` for custom error handling callbacks
39
+ - Fix `reset_context` method to properly reset instance attributes (`@current_user`, `@current_request_uuid`, `@current_remote_addr`)
40
+ - Improve error handling in plugin discovery - gracefully handles missing files, invalid classes, and load errors
41
+ - Add `ActionReporter::PluginDiscovery.reset!` method for testing purposes
42
+ - Add thread safety tests
43
+ - Add error handling tests with fault isolation verification
44
+ - Add comprehensive plugin discovery tests
45
+ - Add custom reporter registration tests
46
+ - Achieve 100% test coverage (297/297 lines)
10
47
 
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`
48
+ ## 1.5.2 (2024-12-13)
14
49
 
15
- # 1.4.1
50
+ - Add Audited context support
16
51
 
17
- * Add paper_trail support
52
+ ## 1.5.1 (2024-10-29)
18
53
 
19
- # 1.4.0
54
+ - Fix Audited current user setter to use `audited_user`
20
55
 
21
- * Set minimum ruby version requirement to 2.5.0
56
+ ## 1.5.0 (2024-10-29)
22
57
 
23
- # 1.3.1
58
+ - BREAKING: Rename `audited_user` to `current_user`
59
+ - Add `current_request_uuid` and `current_remote_addr` getters and setters
60
+ - Memoize `current_user`, `current_request_uuid`, and `current_remote_addr`
24
61
 
25
- * Update gem configuration
62
+ ## 1.4.1 (2024-02-02)
26
63
 
27
- # 1.3.0
64
+ - Add paper_trail support
28
65
 
29
- * Update ruby version to 3.2.2
30
- * Update dependencies to latest
66
+ ## 1.4.0 (2023-08-30)
31
67
 
32
- # 1.2.0
68
+ - Set minimum ruby version requirement to 2.5.0
33
69
 
34
- * Major fixes for class resolvers
35
- * Implemented ActionReporter::Error class
36
- * Improved test coverage
70
+ ## 1.3.2 (2023-08-29)
37
71
 
38
- # 1.1.1
72
+ ## 1.3.1 (2023-08-29)
39
73
 
40
- * Update check-in logic
74
+ - Update gem configuration
41
75
 
42
- # 1.1.0
76
+ ## 1.3.0 (2023-08-15)
43
77
 
44
- * Add reporter check-in method
78
+ - Update ruby version to 3.2.2
79
+ - Update dependencies to latest
45
80
 
46
- # 1.0.7
81
+ ## 1.2.0 (2023-04-20)
47
82
 
48
- * Moving Sentry context under `context` key
83
+ - Major fixes for class resolvers
84
+ - Implemented ActionReporter::Error class
85
+ - Improved test coverage
49
86
 
50
- # 1.0.6
87
+ ## 1.1.1 (2023-04-18)
51
88
 
52
- * Possible fix for Sentry context setting
89
+ - Update check-in logic
53
90
 
54
- # 1.0.5
91
+ ## 1.1.0 (2023-04-18)
55
92
 
56
- * Fix Sentry reporting and context setting
93
+ - Add reporter check-in method
57
94
 
58
- # 1.0.4
95
+ ## 1.0.7 (2023-04-18)
59
96
 
60
- * Move `transform_context` to individual reporter classes
97
+ - Moving Sentry context under `context` key
61
98
 
62
- # 1.0.3
99
+ ## 1.0.6 (2023-04-18)
63
100
 
64
- * Fix scoutapm notice method
101
+ - Possible fix for Sentry context setting
65
102
 
66
- # 1.0.2
103
+ ## 1.0.5 (2023-04-18)
67
104
 
68
- * Add ruby version support for versions lower than 3.2.0
105
+ - Fix Sentry reporting and context setting
69
106
 
70
- # 1.0.1
107
+ ## 1.0.4 (2023-04-18)
71
108
 
72
- * Fix scoutapm reset_context method
73
- * Update README notes
109
+ - Move `transform_context` to individual reporter classes
74
110
 
75
- # 1.0.0
111
+ ## 1.0.3 (2023-04-18)
76
112
 
77
- * Initial version
113
+ - Fix scoutapm notice method
114
+
115
+ ## 1.0.2 (2023-04-18)
116
+
117
+ - Add ruby version support for versions lower than 3.2.0
118
+
119
+ ## 1.0.1 (2023-04-18)
120
+
121
+ - Fix scoutapm reset_context method
122
+ - Update README notes
123
+
124
+ ## 1.0.0 (2023-04-18)
125
+
126
+ - 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 generate
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
 
42
+ gem.add_development_dependency "bundler", "~> 2"
40
43
  gem.add_development_dependency "rspec", "~> 3"
41
44
  gem.add_development_dependency "rspec_junit_formatter", "~> 0.6"
42
45
  gem.add_development_dependency "webmock", "~> 3"
43
- gem.add_development_dependency "pry", "~> 0.14"
44
- gem.add_development_dependency "simplecov", "~> 0.21"
45
- gem.add_development_dependency "simplecov-cobertura", "~> 2"
46
+ gem.add_development_dependency "pry", "~> 0.15"
47
+ gem.add_development_dependency "simplecov", "~> 0.22"
48
+ gem.add_development_dependency "simplecov-cobertura", "~> 3"
49
+ gem.add_development_dependency "standard", "~> 1"
50
+ gem.add_development_dependency "appraisal", "~> 2"
51
+ gem.add_development_dependency "rbs", "~> 3"
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,16 +7,10 @@ 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
12
- if gem_spec
13
- gem_name, version = gem_spec.scan(/([^(\s]+)\s*(?:\(([^)]+)\))?/).first
14
- latest_spec = Gem.loaded_specs[gem_name]
15
- 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
17
- end
18
- Object.const_get(class_name)
19
- end
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] ||= Object.const_get(class_name)
20
14
  end
21
15
  end
22
16
 
@@ -36,7 +30,7 @@ module ActionReporter
36
30
  elsif identifier.respond_to?(:to_s)
37
31
  identifier.to_s
38
32
  else
39
- raise ArgumentError.new("Unknown check-in identifier: #{identifier.inspect}")
33
+ raise ActionReporter::Error.new("Unknown check-in identifier: #{identifier.inspect}")
40
34
  end
41
35
  end
42
36
 
@@ -48,5 +42,11 @@ module ActionReporter
48
42
 
49
43
  def reset_context
50
44
  end
45
+
46
+ def transaction_id=(transaction_id)
47
+ end
48
+
49
+ def transaction_name=(transaction_name)
50
+ end
51
51
  end
52
52
  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.1"
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,13 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -94,6 +93,20 @@ dependencies:
94
93
  - - "~>"
95
94
  - !ruby/object:Gem::Version
96
95
  version: '15'
96
+ - !ruby/object:Gem::Dependency
97
+ name: bundler
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2'
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: rspec
99
112
  requirement: !ruby/object:Gem::Requirement
@@ -142,30 +155,58 @@ dependencies:
142
155
  requirements:
143
156
  - - "~>"
144
157
  - !ruby/object:Gem::Version
145
- version: '0.14'
158
+ version: '0.15'
146
159
  type: :development
147
160
  prerelease: false
148
161
  version_requirements: !ruby/object:Gem::Requirement
149
162
  requirements:
150
163
  - - "~>"
151
164
  - !ruby/object:Gem::Version
152
- version: '0.14'
165
+ version: '0.15'
153
166
  - !ruby/object:Gem::Dependency
154
167
  name: simplecov
155
168
  requirement: !ruby/object:Gem::Requirement
156
169
  requirements:
157
170
  - - "~>"
158
171
  - !ruby/object:Gem::Version
159
- version: '0.21'
172
+ version: '0.22'
160
173
  type: :development
161
174
  prerelease: false
162
175
  version_requirements: !ruby/object:Gem::Requirement
163
176
  requirements:
164
177
  - - "~>"
165
178
  - !ruby/object:Gem::Version
166
- version: '0.21'
179
+ version: '0.22'
167
180
  - !ruby/object:Gem::Dependency
168
181
  name: simplecov-cobertura
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '3'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '3'
194
+ - !ruby/object:Gem::Dependency
195
+ name: standard
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: '1'
201
+ type: :development
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: '1'
208
+ - !ruby/object:Gem::Dependency
209
+ name: appraisal
169
210
  requirement: !ruby/object:Gem::Requirement
170
211
  requirements:
171
212
  - - "~>"
@@ -178,9 +219,23 @@ dependencies:
178
219
  - - "~>"
179
220
  - !ruby/object:Gem::Version
180
221
  version: '2'
222
+ - !ruby/object:Gem::Dependency
223
+ name: rbs
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - "~>"
227
+ - !ruby/object:Gem::Version
228
+ version: '3'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - "~>"
234
+ - !ruby/object:Gem::Version
235
+ version: '3'
181
236
  description: Ruby wrapper for multiple reporting services
182
237
  email:
183
- - andrei@kiskolabs.com
238
+ - contact@kiskolabs.com
184
239
  executables: []
185
240
  extensions: []
186
241
  extra_rdoc_files: []
@@ -192,13 +247,16 @@ files:
192
247
  - lib/action_reporter.rb
193
248
  - lib/action_reporter/audited_reporter.rb
194
249
  - lib/action_reporter/base.rb
250
+ - lib/action_reporter/current.rb
195
251
  - lib/action_reporter/error.rb
196
252
  - lib/action_reporter/honeybadger_reporter.rb
197
253
  - lib/action_reporter/paper_trail_reporter.rb
254
+ - lib/action_reporter/plugin_discovery.rb
198
255
  - lib/action_reporter/rails_reporter.rb
199
256
  - lib/action_reporter/scout_apm_reporter.rb
200
257
  - lib/action_reporter/sentry_reporter.rb
201
258
  - lib/action_reporter/utils.rb
259
+ - lib/action_reporter/version.rb
202
260
  homepage: https://github.com/amkisko/action_reporter.rb
203
261
  licenses:
204
262
  - MIT
@@ -208,7 +266,6 @@ metadata:
208
266
  bug_tracker_uri: https://github.com/amkisko/action_reporter.rb/issues
209
267
  changelog_uri: https://github.com/amkisko/action_reporter.rb/blob/main/CHANGELOG.md
210
268
  rubygems_mfa_required: 'true'
211
- post_install_message:
212
269
  rdoc_options: []
213
270
  require_paths:
214
271
  - lib
@@ -223,8 +280,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
280
  - !ruby/object:Gem::Version
224
281
  version: '0'
225
282
  requirements: []
226
- rubygems_version: 3.5.11
227
- signing_key:
283
+ rubygems_version: 3.6.9
228
284
  specification_version: 4
229
285
  summary: See description
230
286
  test_files: []