findbug 0.2.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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +375 -0
  6. data/Rakefile +12 -0
  7. data/app/controllers/findbug/application_controller.rb +105 -0
  8. data/app/controllers/findbug/dashboard_controller.rb +93 -0
  9. data/app/controllers/findbug/errors_controller.rb +129 -0
  10. data/app/controllers/findbug/performance_controller.rb +80 -0
  11. data/app/jobs/findbug/alert_job.rb +40 -0
  12. data/app/jobs/findbug/cleanup_job.rb +132 -0
  13. data/app/jobs/findbug/persist_job.rb +158 -0
  14. data/app/models/findbug/error_event.rb +197 -0
  15. data/app/models/findbug/performance_event.rb +237 -0
  16. data/app/views/findbug/dashboard/index.html.erb +199 -0
  17. data/app/views/findbug/errors/index.html.erb +137 -0
  18. data/app/views/findbug/errors/show.html.erb +185 -0
  19. data/app/views/findbug/performance/index.html.erb +168 -0
  20. data/app/views/findbug/performance/show.html.erb +203 -0
  21. data/app/views/layouts/findbug/application.html.erb +601 -0
  22. data/lib/findbug/alerts/channels/base.rb +75 -0
  23. data/lib/findbug/alerts/channels/discord.rb +155 -0
  24. data/lib/findbug/alerts/channels/email.rb +179 -0
  25. data/lib/findbug/alerts/channels/slack.rb +149 -0
  26. data/lib/findbug/alerts/channels/webhook.rb +143 -0
  27. data/lib/findbug/alerts/dispatcher.rb +126 -0
  28. data/lib/findbug/alerts/throttler.rb +110 -0
  29. data/lib/findbug/background_persister.rb +142 -0
  30. data/lib/findbug/capture/context.rb +301 -0
  31. data/lib/findbug/capture/exception_handler.rb +141 -0
  32. data/lib/findbug/capture/exception_subscriber.rb +228 -0
  33. data/lib/findbug/capture/message_handler.rb +104 -0
  34. data/lib/findbug/capture/middleware.rb +247 -0
  35. data/lib/findbug/configuration.rb +381 -0
  36. data/lib/findbug/engine.rb +109 -0
  37. data/lib/findbug/performance/instrumentation.rb +336 -0
  38. data/lib/findbug/performance/transaction.rb +193 -0
  39. data/lib/findbug/processing/data_scrubber.rb +163 -0
  40. data/lib/findbug/rails/controller_methods.rb +152 -0
  41. data/lib/findbug/railtie.rb +222 -0
  42. data/lib/findbug/storage/circuit_breaker.rb +223 -0
  43. data/lib/findbug/storage/connection_pool.rb +134 -0
  44. data/lib/findbug/storage/redis_buffer.rb +285 -0
  45. data/lib/findbug/tasks/findbug.rake +167 -0
  46. data/lib/findbug/version.rb +5 -0
  47. data/lib/findbug.rb +216 -0
  48. data/lib/generators/findbug/install_generator.rb +67 -0
  49. data/lib/generators/findbug/templates/POST_INSTALL +41 -0
  50. data/lib/generators/findbug/templates/create_findbug_error_events.rb +44 -0
  51. data/lib/generators/findbug/templates/create_findbug_performance_events.rb +47 -0
  52. data/lib/generators/findbug/templates/initializer.rb +157 -0
  53. data/sig/findbug.rbs +4 -0
  54. metadata +251 -0
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Findbug
4
+ module Processing
5
+ # DataScrubber removes sensitive data from captured events.
6
+ #
7
+ # WHY SCRUBBING IS CRITICAL
8
+ # =========================
9
+ #
10
+ # Error data often contains sensitive information:
11
+ # - User passwords (in form params)
12
+ # - API keys (in headers)
13
+ # - Credit card numbers (in payment flows)
14
+ # - Personal data (in user context)
15
+ #
16
+ # Even though Findbug is self-hosted, you don't want this data:
17
+ # 1. Stored in your database
18
+ # 2. Visible in the dashboard
19
+ # 3. In logs or backups
20
+ # 4. Accessible to developers who shouldn't see it
21
+ #
22
+ # SCRUBBING STRATEGY
23
+ # ==================
24
+ #
25
+ # We replace sensitive values with "[FILTERED]" rather than removing them.
26
+ # This way you can see that the field existed (helpful for debugging)
27
+ # without exposing the actual value.
28
+ #
29
+ # WHAT WE SCRUB
30
+ # =============
31
+ #
32
+ # 1. Known field names (password, api_key, etc.)
33
+ # 2. Credit card patterns (16 digits)
34
+ # 3. SSN patterns (XXX-XX-XXXX)
35
+ # 4. Sensitive headers (Authorization, Cookie)
36
+ # 5. Custom fields from configuration
37
+ #
38
+ class DataScrubber
39
+ FILTERED = "[FILTERED]"
40
+
41
+ # Credit card patterns (Visa, MasterCard, Amex, etc.)
42
+ CREDIT_CARD_PATTERN = /\b(?:\d{4}[-\s]?){3}\d{4}\b/
43
+
44
+ # SSN pattern
45
+ SSN_PATTERN = /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/
46
+
47
+ # Bearer token in text
48
+ BEARER_TOKEN_PATTERN = /Bearer\s+[A-Za-z0-9\-_.~+\/]+=*/i
49
+
50
+ # API key-like patterns (long alphanumeric strings)
51
+ API_KEY_PATTERN = /\b[A-Za-z0-9]{32,}\b/
52
+
53
+ class << self
54
+ # Scrub an entire event hash
55
+ #
56
+ # @param event [Hash] the event data to scrub
57
+ # @return [Hash] scrubbed event data
58
+ #
59
+ def scrub(event)
60
+ deep_scrub(event)
61
+ end
62
+
63
+ # Scrub a string value for patterns
64
+ #
65
+ # @param value [String] the string to scrub
66
+ # @return [String] scrubbed string
67
+ #
68
+ def scrub_string(value)
69
+ return value unless value.is_a?(String)
70
+
71
+ value = value.dup
72
+
73
+ # Scrub credit card numbers
74
+ value.gsub!(CREDIT_CARD_PATTERN, FILTERED)
75
+
76
+ # Scrub SSN
77
+ value.gsub!(SSN_PATTERN, FILTERED)
78
+
79
+ # Scrub Bearer tokens
80
+ value.gsub!(BEARER_TOKEN_PATTERN, "Bearer #{FILTERED}")
81
+
82
+ # Scrub potential API keys (but not in backtraces)
83
+ # Only scrub in certain contexts to avoid false positives
84
+ # value.gsub!(API_KEY_PATTERN, FILTERED)
85
+
86
+ value
87
+ end
88
+
89
+ private
90
+
91
+ def deep_scrub(obj, path = [])
92
+ case obj
93
+ when Hash
94
+ # Preserve original key type (symbol or string)
95
+ obj.each_with_object({}) do |(key, value), result|
96
+ result[key] = if sensitive_key?(key)
97
+ FILTERED
98
+ else
99
+ deep_scrub(value, path + [key])
100
+ end
101
+ end
102
+ when Array
103
+ obj.map.with_index { |item, i| deep_scrub(item, path + [i]) }
104
+ when String
105
+ scrub_string(obj)
106
+ else
107
+ obj
108
+ end
109
+ end
110
+
111
+ def sensitive_key?(key)
112
+ key_s = key.to_s.downcase
113
+
114
+ # Check against configured scrub fields
115
+ scrub_fields.any? do |field|
116
+ key_s.include?(field.downcase)
117
+ end
118
+ end
119
+
120
+ def scrub_fields
121
+ @scrub_fields ||= build_scrub_fields
122
+ end
123
+
124
+ def build_scrub_fields
125
+ default_fields = %w[
126
+ password
127
+ passwd
128
+ secret
129
+ token
130
+ api_key
131
+ apikey
132
+ access_key
133
+ accesskey
134
+ private_key
135
+ privatekey
136
+ credit_card
137
+ creditcard
138
+ card_number
139
+ cardnumber
140
+ cvv
141
+ cvc
142
+ ssn
143
+ social_security
144
+ authorization
145
+ auth
146
+ bearer
147
+ cookie
148
+ session
149
+ csrf
150
+ ]
151
+
152
+ # Merge with user-configured fields
153
+ (default_fields + Findbug.config.scrub_fields.map(&:to_s)).uniq
154
+ end
155
+
156
+ # Reset cached fields (for testing or config changes)
157
+ def reset!
158
+ @scrub_fields = nil
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Findbug
4
+ module RailsExt
5
+ # ControllerMethods provides helper methods for Rails controllers.
6
+ #
7
+ # These methods are automatically included in all controllers via the Railtie.
8
+ # They let you add custom context to errors and performance data.
9
+ #
10
+ # WHY CONTROLLER HELPERS?
11
+ # =======================
12
+ #
13
+ # When an error occurs, you often want to know:
14
+ # - Which user was affected?
15
+ # - What were the request params?
16
+ # - What was the user's plan/tier?
17
+ # - What A/B experiment variant were they in?
18
+ #
19
+ # These helpers let you attach this context easily:
20
+ #
21
+ # class ApplicationController < ActionController::Base
22
+ # before_action :set_findbug_context
23
+ #
24
+ # def set_findbug_context
25
+ # findbug_set_user(current_user)
26
+ # findbug_set_context(
27
+ # plan: current_user&.plan,
28
+ # experiment: session[:ab_variant]
29
+ # )
30
+ # end
31
+ # end
32
+ #
33
+ # Then when an error occurs, all this context is captured automatically.
34
+ #
35
+ module ControllerMethods
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ # Store context in a thread-local variable
40
+ # Thread-local means each request has its own context
41
+ # This is important for thread-safe operation in Puma
42
+ before_action :findbug_clear_context
43
+ after_action :findbug_clear_context
44
+ end
45
+
46
+ # Set the current user for error context
47
+ #
48
+ # @param user [Object] the user object (any object with id, email, etc.)
49
+ #
50
+ # @example
51
+ # findbug_set_user(current_user)
52
+ #
53
+ # WHY A SEPARATE USER METHOD?
54
+ # ---------------------------
55
+ # Users are special - they're the most common context and have
56
+ # special handling (we extract id, email, username automatically).
57
+ #
58
+ def findbug_set_user(user)
59
+ return unless user
60
+
61
+ Findbug::Capture::Context.set_user(
62
+ id: user.try(:id),
63
+ email: user.try(:email),
64
+ username: user.try(:username) || user.try(:name)
65
+ )
66
+ end
67
+
68
+ # Set custom context data
69
+ #
70
+ # @param data [Hash] key-value pairs to attach to errors
71
+ #
72
+ # @example
73
+ # findbug_set_context(
74
+ # organization_id: current_org.id,
75
+ # feature_flags: current_flags
76
+ # )
77
+ #
78
+ def findbug_set_context(data = {})
79
+ Findbug::Capture::Context.merge(data)
80
+ end
81
+
82
+ # Add a tag (short key-value pair for filtering)
83
+ #
84
+ # @param key [String, Symbol] the tag key
85
+ # @param value [String] the tag value
86
+ #
87
+ # @example
88
+ # findbug_tag(:environment, "production")
89
+ # findbug_tag(:region, "us-east-1")
90
+ #
91
+ # Tags are optimized for filtering/grouping in the dashboard.
92
+ # Use context for detailed data, tags for filterable attributes.
93
+ #
94
+ def findbug_tag(key, value)
95
+ Findbug::Capture::Context.add_tag(key, value)
96
+ end
97
+
98
+ # Add a breadcrumb (for debugging what happened before the error)
99
+ #
100
+ # @param message [String] what happened
101
+ # @param category [String] category for grouping
102
+ # @param data [Hash] additional data
103
+ #
104
+ # @example
105
+ # findbug_breadcrumb("User logged in", category: "auth")
106
+ # findbug_breadcrumb("Loaded products", category: "query", data: { count: 50 })
107
+ #
108
+ # Breadcrumbs help you understand the sequence of events leading to an error.
109
+ # Think of them like a trail of breadcrumbs Hansel & Gretel left.
110
+ #
111
+ def findbug_breadcrumb(message, category: "default", data: {})
112
+ Findbug::Capture::Context.add_breadcrumb(
113
+ message: message,
114
+ category: category,
115
+ data: data,
116
+ timestamp: Time.now.utc.iso8601(3)
117
+ )
118
+ end
119
+
120
+ # Capture an exception manually with current context
121
+ #
122
+ # @param exception [Exception] the exception to capture
123
+ # @param extra [Hash] additional context for this specific error
124
+ #
125
+ # @example
126
+ # begin
127
+ # external_api.call
128
+ # rescue ExternalAPIError => e
129
+ # findbug_capture(e, api: "payment_gateway")
130
+ # # handle gracefully
131
+ # end
132
+ #
133
+ def findbug_capture(exception, extra = {})
134
+ Findbug.capture_exception(exception, extra)
135
+ end
136
+
137
+ private
138
+
139
+ # Clear context between requests
140
+ #
141
+ # WHY CLEAR CONTEXT?
142
+ # ------------------
143
+ # Without clearing, context from one request could leak into another.
144
+ # This is especially important in threaded servers like Puma where
145
+ # threads are reused across requests.
146
+ #
147
+ def findbug_clear_context
148
+ Findbug::Capture::Context.clear!
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Findbug
6
+ # Railtie hooks Findbug into the Rails boot process.
7
+ #
8
+ # WHAT IS A RAILTIE?
9
+ # ==================
10
+ #
11
+ # When Rails boots, it looks for classes that inherit from Rails::Railtie
12
+ # and calls their initializers in order. This is how gems integrate with Rails.
13
+ #
14
+ # Common things Railties do:
15
+ # - Insert middleware into the stack
16
+ # - Subscribe to ActiveSupport::Notifications
17
+ # - Add rake tasks
18
+ # - Configure the Rails app
19
+ #
20
+ # WHY USE A RAILTIE?
21
+ # ==================
22
+ #
23
+ # Instead of making users add Findbug to 5 different places:
24
+ #
25
+ # # application.rb
26
+ # config.middleware.use Findbug::Capture::Middleware
27
+ #
28
+ # # initializer
29
+ # ActiveSupport::Notifications.subscribe(...)
30
+ #
31
+ # # routes
32
+ # mount Findbug::Web::Engine => "/findbug"
33
+ #
34
+ # We do it all automatically in the Railtie. User just adds the gem
35
+ # and creates a config file. Zero setup!
36
+ #
37
+ # THE INITIALIZATION ORDER
38
+ # ========================
39
+ #
40
+ # Rails runs initializers in stages:
41
+ #
42
+ # 1. before_configuration - Before config is read
43
+ # 2. before_initialize - Before Rails.initialize!
44
+ # 3. to_prepare - Before each request (dev) or once (prod)
45
+ # 4. after_initialize - After Rails is fully loaded
46
+ #
47
+ # We use after_initialize because we need:
48
+ # - Rails.env to be set
49
+ # - Database connections to exist
50
+ # - All models to be loaded
51
+ #
52
+ class Railtie < Rails::Railtie
53
+ # Register our middleware to catch exceptions
54
+ #
55
+ # MIDDLEWARE ORDER MATTERS!
56
+ # -------------------------
57
+ #
58
+ # We insert AFTER ActionDispatch::ShowExceptions because:
59
+ # 1. ShowExceptions converts exceptions to HTTP responses
60
+ # 2. We want to capture the raw exception BEFORE that happens
61
+ # 3. We also want to capture exceptions that ShowExceptions misses
62
+ #
63
+ # Stack (simplified):
64
+ # Rails::Rack::Logger
65
+ # ActionDispatch::RequestId
66
+ # ActionDispatch::ShowExceptions ← Converts exceptions to 500 pages
67
+ # Findbug::Capture::Middleware ← WE GO HERE (sees raw exceptions)
68
+ # ActionDispatch::Routing
69
+ # YourController#action
70
+ #
71
+ initializer "findbug.middleware" do
72
+ require_relative "capture/middleware"
73
+
74
+ Rails.application.config.middleware.use(Findbug::Capture::Middleware)
75
+ end
76
+
77
+ # Set up Rails error reporting integration (Rails 7+)
78
+ #
79
+ # Rails 7 introduced ErrorReporter for centralized error handling.
80
+ # We subscribe to it so we capture ALL errors, even those handled
81
+ # gracefully by the app.
82
+ #
83
+ initializer "findbug.error_reporter" do |app|
84
+ require_relative "capture/exception_subscriber"
85
+
86
+ app.config.after_initialize do
87
+ if defined?(Rails.error) && Rails.error.respond_to?(:subscribe)
88
+ Rails.error.subscribe(Findbug::Capture::ExceptionSubscriber.new)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Set up performance instrumentation
94
+ #
95
+ # Rails uses ActiveSupport::Notifications for internal events:
96
+ # - sql.active_record (database queries)
97
+ # - process_action.action_controller (requests)
98
+ # - render_template.action_view (view rendering)
99
+ #
100
+ # We subscribe to these to capture performance data.
101
+ #
102
+ initializer "findbug.instrumentation" do |app|
103
+ app.config.after_initialize do
104
+ next unless Findbug.config.performance_enabled
105
+
106
+ require_relative "performance/instrumentation"
107
+ Findbug::Performance::Instrumentation.setup!
108
+ end
109
+ end
110
+
111
+ # Mount the web dashboard engine
112
+ #
113
+ # This adds routes for the /findbug dashboard.
114
+ # We only mount if authentication is configured (security!).
115
+ #
116
+ initializer "findbug.routes" do |app|
117
+ app.config.after_initialize do
118
+ next unless Findbug.config.web_enabled?
119
+
120
+ require_relative "engine"
121
+
122
+ # Add routes programmatically
123
+ # This is equivalent to `mount Findbug::Engine => "/findbug"` in routes.rb
124
+ # but automatic!
125
+ app.routes.append do
126
+ mount Findbug::Engine => Findbug.config.web_path
127
+ end
128
+ end
129
+ end
130
+
131
+ # Set up default configuration values that depend on Rails
132
+ #
133
+ initializer "findbug.defaults" do |app|
134
+ app.config.after_initialize do
135
+ config = Findbug.config
136
+
137
+ # Use Rails.env if environment not explicitly set
138
+ config.environment ||= Rails.env
139
+
140
+ # Disable in test environment by default
141
+ if Rails.env.test? && config.enabled
142
+ Findbug.logger.debug(
143
+ "[Findbug] Running in test environment. Set `config.enabled = true` to enable."
144
+ )
145
+ # Note: We don't force disable here. User might want it enabled for integration tests.
146
+ end
147
+
148
+ # Try to auto-detect release from common sources
149
+ config.release ||= detect_release
150
+ end
151
+ end
152
+
153
+ # Add Findbug helpers to ActionController
154
+ #
155
+ # This adds methods like `findbug_context` that controllers can use
156
+ # to add custom context to errors.
157
+ #
158
+ initializer "findbug.controller_methods" do
159
+ ActiveSupport.on_load(:action_controller) do
160
+ require_relative "rails/controller_methods"
161
+ include Findbug::RailsExt::ControllerMethods
162
+ end
163
+ end
164
+
165
+ # Start background persister
166
+ #
167
+ # This runs a thread that periodically moves events from Redis to the database.
168
+ # Users don't need to set up Sidekiq or any job system - it works out of the box.
169
+ #
170
+ initializer "findbug.background_persister" do |app|
171
+ app.config.after_initialize do
172
+ next unless Findbug.enabled?
173
+ next unless Findbug.config.auto_persist
174
+
175
+ require_relative "background_persister"
176
+ Findbug::BackgroundPersister.start!(
177
+ interval: Findbug.config.persist_interval
178
+ )
179
+ end
180
+ end
181
+
182
+ # Register cleanup for application shutdown
183
+ #
184
+ # When the app shuts down (e.g., during deploys), we want to:
185
+ # 1. Stop the background persister thread
186
+ # 2. Flush any pending events
187
+ # 3. Close Redis connections cleanly
188
+ #
189
+ initializer "findbug.shutdown" do |app|
190
+ at_exit do
191
+ Findbug::BackgroundPersister.stop! if defined?(Findbug::BackgroundPersister)
192
+ Findbug::Storage::ConnectionPool.shutdown! if defined?(Findbug::Storage::ConnectionPool)
193
+ end
194
+ end
195
+
196
+ # Add rake tasks
197
+ rake_tasks do
198
+ load File.expand_path("tasks/findbug.rake", __dir__)
199
+ end
200
+
201
+ private
202
+
203
+ # Try to detect the release/version from environment
204
+ def detect_release
205
+ # Common environment variables for release tracking
206
+ ENV["FINDBUG_RELEASE"] ||
207
+ ENV["HEROKU_SLUG_COMMIT"] ||
208
+ ENV["RENDER_GIT_COMMIT"] ||
209
+ ENV["GIT_COMMIT"] ||
210
+ ENV["SOURCE_VERSION"] ||
211
+ git_sha
212
+ end
213
+
214
+ # Get current git SHA (if in a git repo)
215
+ def git_sha
216
+ sha = `git rev-parse --short HEAD 2>/dev/null`.strip
217
+ sha.empty? ? nil : sha
218
+ rescue StandardError
219
+ nil
220
+ end
221
+ end
222
+ end