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,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Findbug Rake Tasks
4
+ #
5
+ # These tasks help with maintenance and debugging.
6
+ # Run `rake -T findbug` to see all available tasks.
7
+ #
8
+
9
+ namespace :findbug do
10
+ desc "Show Findbug configuration and status"
11
+ task status: :environment do
12
+ config = Findbug.config
13
+
14
+ puts "\n=== Findbug Status ==="
15
+ puts ""
16
+ puts "Enabled: #{Findbug.enabled? ? 'Yes' : 'No'}"
17
+ puts "Environment: #{config.environment}"
18
+ puts "Release: #{config.release || '(not set)'}"
19
+ puts ""
20
+
21
+ puts "--- Error Capture ---"
22
+ puts "Sample Rate: #{(config.sample_rate * 100).round(1)}%"
23
+ puts "Ignored Exceptions: #{config.ignored_exceptions.map(&:name).join(', ').presence || '(none)'}"
24
+ puts ""
25
+
26
+ puts "--- Performance ---"
27
+ puts "Performance Enabled: #{config.performance_enabled ? 'Yes' : 'No'}"
28
+ puts "Performance Sample: #{(config.performance_sample_rate * 100).round(1)}%"
29
+ puts "Slow Request Threshold: #{config.slow_request_threshold_ms}ms"
30
+ puts "Slow Query Threshold: #{config.slow_query_threshold_ms}ms"
31
+ puts ""
32
+
33
+ puts "--- Storage ---"
34
+ puts "Redis URL: #{config.redis_url.gsub(/:[^@]+@/, ':***@')}" # Hide password
35
+ puts "Redis Pool Size: #{config.redis_pool_size}"
36
+ puts "Retention Days: #{config.retention_days}"
37
+ puts ""
38
+
39
+ puts "--- Dashboard ---"
40
+ puts "Dashboard Enabled: #{config.web_enabled? ? 'Yes' : 'No'}"
41
+ puts "Dashboard Path: #{config.web_path}" if config.web_enabled?
42
+ puts ""
43
+
44
+ puts "--- Alerts ---"
45
+ if config.alerts.any_enabled?
46
+ config.alerts.enabled_channels.each do |name, _|
47
+ puts " #{name}: enabled"
48
+ end
49
+ else
50
+ puts "(no alerts configured)"
51
+ end
52
+
53
+ puts ""
54
+
55
+ # Show Redis buffer stats if available
56
+ if Findbug.enabled?
57
+ puts "--- Buffer Status ---"
58
+ begin
59
+ stats = Findbug::Storage::RedisBuffer.stats
60
+ puts "Error Queue Length: #{stats[:error_queue_length]}"
61
+ puts "Performance Queue Length: #{stats[:performance_queue_length]}"
62
+ puts "Circuit Breaker State: #{stats[:circuit_breaker_state]}"
63
+ puts "Circuit Breaker Failures: #{stats[:circuit_breaker_failures]}"
64
+ rescue StandardError => e
65
+ puts "Could not fetch buffer stats: #{e.message}"
66
+ end
67
+ end
68
+
69
+ puts "\n"
70
+ end
71
+
72
+ desc "Clear all Findbug data from Redis buffers"
73
+ task clear_buffers: :environment do
74
+ puts "Clearing Findbug Redis buffers..."
75
+ Findbug::Storage::RedisBuffer.clear!
76
+ puts "Done!"
77
+ end
78
+
79
+ desc "Flush Redis buffers to database immediately"
80
+ task flush: :environment do
81
+ puts "Flushing Findbug buffers to database..."
82
+
83
+ require_relative "../jobs/persist_job"
84
+
85
+ error_count = 0
86
+ perf_count = 0
87
+
88
+ loop do
89
+ events = Findbug::Storage::RedisBuffer.pop_errors(100)
90
+ break if events.empty?
91
+
92
+ Findbug::Jobs::PersistJob.persist_errors(events)
93
+ error_count += events.size
94
+ end
95
+
96
+ loop do
97
+ events = Findbug::Storage::RedisBuffer.pop_performance(100)
98
+ break if events.empty?
99
+
100
+ Findbug::Jobs::PersistJob.persist_performance(events)
101
+ perf_count += events.size
102
+ end
103
+
104
+ puts "Flushed #{error_count} error events and #{perf_count} performance events."
105
+ end
106
+
107
+ desc "Run cleanup to remove old records"
108
+ task cleanup: :environment do
109
+ puts "Running Findbug cleanup..."
110
+
111
+ require_relative "../jobs/cleanup_job"
112
+ Findbug::Jobs::CleanupJob.perform_now
113
+
114
+ puts "Done!"
115
+ end
116
+
117
+ desc "Test error capture by raising a test exception"
118
+ task test: :environment do
119
+ puts "Testing Findbug error capture..."
120
+
121
+ # Capture a test exception
122
+ begin
123
+ raise "Findbug Test Exception - #{Time.now}"
124
+ rescue StandardError => e
125
+ Findbug.capture_exception(e, test: true)
126
+ puts "Test exception captured!"
127
+ end
128
+
129
+ # Wait for async write
130
+ sleep 0.5
131
+
132
+ # Check if it made it to Redis
133
+ stats = Findbug::Storage::RedisBuffer.stats
134
+ puts "Error queue length: #{stats[:error_queue_length]}"
135
+
136
+ if stats[:error_queue_length].positive?
137
+ puts "\nTest passed! Exception was captured."
138
+ else
139
+ puts "\nTest may have failed. Check configuration."
140
+ end
141
+ end
142
+
143
+ namespace :db do
144
+ desc "Show database record counts"
145
+ task stats: :environment do
146
+ puts "\n=== Findbug Database Stats ==="
147
+
148
+ if defined?(Findbug::ErrorEvent)
149
+ total_errors = Findbug::ErrorEvent.count
150
+ unresolved = Findbug::ErrorEvent.where(status: "unresolved").count
151
+ puts "Total Errors: #{total_errors}"
152
+ puts "Unresolved: #{unresolved}"
153
+ else
154
+ puts "ErrorEvent model not loaded"
155
+ end
156
+
157
+ if defined?(Findbug::PerformanceEvent)
158
+ total_perf = Findbug::PerformanceEvent.count
159
+ puts "Performance Events: #{total_perf}"
160
+ else
161
+ puts "PerformanceEvent model not loaded"
162
+ end
163
+
164
+ puts ""
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Findbug
4
+ VERSION = "0.2.0"
5
+ end
data/lib/findbug.rb ADDED
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "findbug/version"
4
+ require_relative "findbug/configuration"
5
+
6
+ # Findbug - Self-hosted error tracking and performance monitoring for Rails
7
+ #
8
+ # ARCHITECTURE OVERVIEW
9
+ # =====================
10
+ #
11
+ # Findbug is designed with ONE critical goal: NEVER slow down your application.
12
+ #
13
+ # How we achieve this:
14
+ #
15
+ # 1. ASYNC WRITES
16
+ # When an error occurs, we don't write to the database immediately.
17
+ # Instead, we push to a Redis buffer in a background thread.
18
+ # This takes ~1-2ms and doesn't block your request.
19
+ #
20
+ # 2. BACKGROUND PERSISTENCE
21
+ # A periodic job (via ActiveJob or built-in thread) pulls events from Redis
22
+ # and batch-inserts them to the database. This happens outside your
23
+ # request cycle.
24
+ #
25
+ # 3. CIRCUIT BREAKER
26
+ # If Redis is down, we don't keep retrying and slowing down your app.
27
+ # The circuit breaker "opens" after 5 failures and stops attempting
28
+ # writes for 30 seconds.
29
+ #
30
+ # 4. CONNECTION POOLING
31
+ # We maintain our OWN Redis connection pool, separate from your app's
32
+ # Redis/Sidekiq. This prevents connection contention.
33
+ #
34
+ # 5. SAMPLING
35
+ # For high-traffic apps, you can sample errors (e.g., capture 50%)
36
+ # to reduce overhead further.
37
+ #
38
+ # DATA FLOW
39
+ # =========
40
+ #
41
+ # [Exception occurs]
42
+ # |
43
+ # v
44
+ # [Middleware catches it]
45
+ # |
46
+ # v
47
+ # [Scrub sensitive data]
48
+ # |
49
+ # v
50
+ # [Push to Redis buffer] <-- Async, non-blocking (Thread.new)
51
+ # |
52
+ # v
53
+ # [BackgroundPersister runs every 30s]
54
+ # |
55
+ # v
56
+ # [Batch insert to Database]
57
+ # |
58
+ # v
59
+ # [Dashboard displays data]
60
+ #
61
+ module Findbug
62
+ # Base error class for Findbug-specific exceptions
63
+ class Error < StandardError; end
64
+
65
+ class << self
66
+ # Access the configuration object
67
+ #
68
+ # @return [Configuration] the current configuration
69
+ #
70
+ # WHY A CLASS METHOD?
71
+ # -------------------
72
+ # We use `Findbug.config` instead of a global variable because:
73
+ # 1. It's lazily initialized (created on first access)
74
+ # 2. It's thread-safe (||= is atomic in MRI Ruby)
75
+ # 3. It's mockable in tests
76
+ # 4. It follows Ruby conventions (like Rails.config)
77
+ #
78
+ def config
79
+ @config ||= Configuration.new
80
+ end
81
+
82
+ # Configure Findbug with a block
83
+ #
84
+ # @yield [Configuration] the configuration object
85
+ #
86
+ # @example
87
+ # Findbug.configure do |config|
88
+ # config.redis_url = "redis://localhost:6379/1"
89
+ # config.sample_rate = 0.5
90
+ #
91
+ # config.alerts do |alerts|
92
+ # alerts.slack enabled: true, webhook_url: ENV["SLACK_WEBHOOK"]
93
+ # end
94
+ # end
95
+ #
96
+ def configure
97
+ yield(config) if block_given?
98
+ config.validate!
99
+ config
100
+ end
101
+
102
+ # Reset configuration to defaults (useful for testing)
103
+ #
104
+ # WHY EXPOSE THIS?
105
+ # ----------------
106
+ # In tests, you often want to reset state between examples.
107
+ # This makes Findbug test-friendly.
108
+ #
109
+ def reset!
110
+ @config = nil
111
+ @redis_pool = nil
112
+ @logger = nil
113
+ end
114
+
115
+ # Get the logger instance
116
+ #
117
+ # Falls back to Rails.logger, then to a null logger
118
+ #
119
+ def logger
120
+ @logger ||= config.logger || (defined?(Rails) && Rails.logger) || Logger.new(IO::NULL)
121
+ end
122
+
123
+ # Set a custom logger
124
+ def logger=(new_logger)
125
+ @logger = new_logger
126
+ end
127
+
128
+ # Check if Findbug is enabled
129
+ #
130
+ # This is a convenience method used throughout the codebase.
131
+ # It checks both the enabled flag AND validates we're properly configured.
132
+ #
133
+ def enabled?
134
+ config.enabled && config.redis_url.present?
135
+ end
136
+
137
+ # Capture an exception manually
138
+ #
139
+ # @param exception [Exception] the exception to capture
140
+ # @param context [Hash] additional context to attach
141
+ #
142
+ # @example
143
+ # begin
144
+ # risky_operation
145
+ # rescue => e
146
+ # Findbug.capture_exception(e, user_id: current_user.id)
147
+ # raise # re-raise to let Rails handle it
148
+ # end
149
+ #
150
+ # WHY A MANUAL CAPTURE METHOD?
151
+ # ----------------------------
152
+ # Sometimes you want to capture an exception without crashing.
153
+ # For example, in a rescue block where you handle the error
154
+ # gracefully but still want to track it.
155
+ #
156
+ def capture_exception(exception, context = {})
157
+ return unless enabled?
158
+ return unless config.should_capture_exception?(exception)
159
+
160
+ Capture::ExceptionHandler.capture(exception, context)
161
+ rescue StandardError => e
162
+ # CRITICAL: Never let Findbug crash your app
163
+ logger.error("[Findbug] Failed to capture exception: #{e.message}")
164
+ end
165
+
166
+ # Capture a message (non-exception event)
167
+ #
168
+ # @param message [String] the message to capture
169
+ # @param level [Symbol] severity level (:info, :warning, :error)
170
+ # @param context [Hash] additional context
171
+ #
172
+ # @example
173
+ # Findbug.capture_message("User exceeded rate limit", :warning, user_id: 123)
174
+ #
175
+ def capture_message(message, level = :info, context = {})
176
+ return unless enabled?
177
+
178
+ Capture::MessageHandler.capture(message, level, context)
179
+ rescue StandardError => e
180
+ logger.error("[Findbug] Failed to capture message: #{e.message}")
181
+ end
182
+
183
+ # Wrap a block with performance tracking
184
+ #
185
+ # @param name [String] name for this operation
186
+ # @yield the block to track
187
+ #
188
+ # @example
189
+ # Findbug.track_performance("external_api_call") do
190
+ # ExternalAPI.fetch_data
191
+ # end
192
+ #
193
+ def track_performance(name, &block)
194
+ return yield unless enabled? && config.performance_enabled
195
+
196
+ Performance::Transaction.track(name, &block)
197
+ rescue StandardError => e
198
+ logger.error("[Findbug] Performance tracking failed: #{e.message}")
199
+ yield # Still execute the block even if tracking fails
200
+ end
201
+ end
202
+ end
203
+
204
+ # Load core library modules (these stay in lib/ as they're not Rails-autoloadable)
205
+ require_relative "findbug/storage/connection_pool"
206
+ require_relative "findbug/storage/circuit_breaker"
207
+ require_relative "findbug/storage/redis_buffer"
208
+ require_relative "findbug/processing/data_scrubber"
209
+ require_relative "findbug/capture/context"
210
+ require_relative "findbug/capture/exception_handler"
211
+ require_relative "findbug/capture/message_handler"
212
+ require_relative "findbug/capture/middleware"
213
+
214
+ # Load the Railtie if Rails is available
215
+ # This auto-configures Findbug when Rails boots
216
+ require_relative "findbug/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Findbug
7
+ module Generators
8
+ # InstallGenerator sets up Findbug in a Rails application.
9
+ #
10
+ # Usage:
11
+ # rails generate findbug:install
12
+ #
13
+ # This will:
14
+ # 1. Create the initializer (config/initializers/findbug.rb)
15
+ # 2. Create database migrations
16
+ # 3. Display next steps
17
+ #
18
+ class InstallGenerator < Rails::Generators::Base
19
+ include Rails::Generators::Migration
20
+
21
+ source_root File.expand_path("templates", __dir__)
22
+
23
+ desc "Install Findbug: creates initializer and migrations"
24
+
25
+ class_option :skip_migrations,
26
+ type: :boolean,
27
+ default: false,
28
+ desc: "Skip creating migrations"
29
+
30
+ def self.next_migration_number(dirname)
31
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
32
+ end
33
+
34
+ def create_initializer
35
+ template "initializer.rb", "config/initializers/findbug.rb"
36
+ say_status :create, "config/initializers/findbug.rb", :green
37
+ end
38
+
39
+ def create_migrations
40
+ return if options[:skip_migrations]
41
+
42
+ migration_template(
43
+ "create_findbug_error_events.rb",
44
+ "db/migrate/create_findbug_error_events.rb"
45
+ )
46
+
47
+ migration_template(
48
+ "create_findbug_performance_events.rb",
49
+ "db/migrate/create_findbug_performance_events.rb"
50
+ )
51
+
52
+ say_status :create, "db/migrate/create_findbug_error_events.rb", :green
53
+ say_status :create, "db/migrate/create_findbug_performance_events.rb", :green
54
+ end
55
+
56
+ def display_post_install
57
+ readme "POST_INSTALL" if behavior == :invoke
58
+ end
59
+
60
+ private
61
+
62
+ def migration_version
63
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+
2
+ ===============================================================================
3
+ Findbug has been installed successfully!
4
+ ===============================================================================
5
+
6
+ Next steps:
7
+
8
+ 1. Run the migrations:
9
+
10
+ rails db:migrate
11
+
12
+ 2. Configure Redis URL (if different from default):
13
+
14
+ # Add to your environment variables or .env file:
15
+ FINDBUG_REDIS_URL=redis://localhost:6379/1
16
+
17
+ # Or set directly in config/initializers/findbug.rb:
18
+ config.redis_url = "redis://localhost:6379/1"
19
+
20
+ 3. Configure dashboard authentication (required):
21
+
22
+ # Add to your environment variables or .env file:
23
+ FINDBUG_USERNAME=admin
24
+ FINDBUG_PASSWORD=your-secure-password
25
+
26
+ 5. Access the dashboard:
27
+
28
+ http://localhost:3000/findbug
29
+
30
+ 6. (Optional) Configure alerts in config/initializers/findbug.rb:
31
+
32
+ config.alerts do |alerts|
33
+ alerts.slack(
34
+ enabled: true,
35
+ webhook_url: ENV["SLACK_WEBHOOK_URL"]
36
+ )
37
+ end
38
+
39
+ For more information, see: https://github.com/ITSSOUMIT/findbug
40
+
41
+ ===============================================================================
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateFindbugErrorEvents < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :findbug_error_events do |t|
6
+ # Error identification
7
+ t.string :fingerprint, null: false
8
+ t.string :exception_class, null: false
9
+ t.text :message
10
+ t.text :backtrace
11
+
12
+ # Context (stored as JSON for flexibility)
13
+ t.jsonb :context, default: {}
14
+ t.jsonb :request_data, default: {}
15
+
16
+ # Metadata
17
+ t.string :environment
18
+ t.string :release_version
19
+ t.string :severity, default: "error"
20
+ t.string :source
21
+ t.boolean :handled, default: false
22
+
23
+ # Aggregation
24
+ t.integer :occurrence_count, default: 1
25
+ t.datetime :first_seen_at
26
+ t.datetime :last_seen_at
27
+
28
+ # Status tracking
29
+ t.string :status, default: "unresolved"
30
+
31
+ t.timestamps
32
+ end
33
+
34
+ # Indexes for common queries
35
+ add_index :findbug_error_events, :fingerprint
36
+ add_index :findbug_error_events, :exception_class
37
+ add_index :findbug_error_events, :status
38
+ add_index :findbug_error_events, :severity
39
+ add_index :findbug_error_events, :last_seen_at
40
+ add_index :findbug_error_events, :created_at
41
+ add_index :findbug_error_events, [:status, :last_seen_at]
42
+ add_index :findbug_error_events, [:exception_class, :created_at]
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateFindbugPerformanceEvents < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :findbug_performance_events do |t|
6
+ # Transaction identification
7
+ t.string :transaction_name, null: false
8
+ t.string :transaction_type, default: "request"
9
+
10
+ # Request info
11
+ t.string :request_method
12
+ t.string :request_path
13
+ t.string :format
14
+ t.integer :status
15
+
16
+ # Timing (all in milliseconds)
17
+ t.float :duration_ms, null: false
18
+ t.float :db_time_ms, default: 0
19
+ t.float :view_time_ms, default: 0
20
+
21
+ # Query tracking
22
+ t.integer :query_count, default: 0
23
+ t.jsonb :slow_queries, default: []
24
+ t.jsonb :n_plus_one_queries, default: []
25
+ t.boolean :has_n_plus_one, default: false
26
+ t.integer :view_count, default: 0
27
+
28
+ # Context
29
+ t.jsonb :context, default: {}
30
+
31
+ # Metadata
32
+ t.string :environment
33
+ t.string :release_version
34
+ t.datetime :captured_at
35
+
36
+ t.timestamps
37
+ end
38
+
39
+ # Indexes for common queries (using short names to stay under 63 char limit)
40
+ add_index :findbug_performance_events, :transaction_name, name: "idx_fb_perf_txn_name"
41
+ add_index :findbug_performance_events, :transaction_type, name: "idx_fb_perf_txn_type"
42
+ add_index :findbug_performance_events, :captured_at, name: "idx_fb_perf_captured_at"
43
+ add_index :findbug_performance_events, :duration_ms, name: "idx_fb_perf_duration"
44
+ add_index :findbug_performance_events, :has_n_plus_one, name: "idx_fb_perf_n_plus_one"
45
+ add_index :findbug_performance_events, [:transaction_name, :captured_at], name: "idx_fb_perf_txn_captured"
46
+ end
47
+ end