Class: RailsErrorDashboard::Services::NplusOneDetector
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Services::NplusOneDetector
- Defined in:
- lib/rails_error_dashboard/services/n_plus_one_detector.rb
Overview
Pure service: Detect N+1 query patterns from SQL breadcrumbs
Analyzes already-captured breadcrumbs at display time (NOT on every request). Groups SQL queries by normalized fingerprint and flags patterns where the same query shape appears >= threshold times.
SAFETY: O(n) over max 40 breadcrumbs, wrapped in rescue => [].
Class Method Summary collapse
-
.call(breadcrumbs, threshold: nil) ⇒ Array<Hash>
Detect N+1 patterns in breadcrumbs.
-
.normalize_sql(sql) ⇒ String
Normalize SQL for fingerprinting Replaces literal values with ? placeholders while preserving structure.
Class Method Details
.call(breadcrumbs, threshold: nil) ⇒ Array<Hash>
Detect N+1 patterns in breadcrumbs
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/rails_error_dashboard/services/n_plus_one_detector.rb', line 18 def call(, threshold: nil) return [] unless .is_a?(Array) && .any? threshold ||= RailsErrorDashboard.configuration.n_plus_one_threshold || 3 # Extract SQL breadcrumbs only sql_crumbs = .select { |c| c["c"] == "sql" } return [] if sql_crumbs.empty? # Group by normalized fingerprint groups = Hash.new { |h, k| h[k] = { count: 0, total_duration_ms: 0.0, sample_query: nil } } sql_crumbs.each do |crumb| sql = crumb["m"].to_s next if sql.empty? fingerprint = normalize_sql(sql) group = groups[fingerprint] group[:count] += 1 group[:total_duration_ms] += crumb["d"].to_f group[:sample_query] ||= sql end # Filter by threshold and sort by count desc groups .select { |_, v| v[:count] >= threshold } .map { |fingerprint, v| { fingerprint: fingerprint, count: v[:count], total_duration_ms: v[:total_duration_ms].round(2), sample_query: v[:sample_query] } } .sort_by { |p| -p[:count] } rescue => e [] end |
.normalize_sql(sql) ⇒ String
Normalize SQL for fingerprinting Replaces literal values with ? placeholders while preserving structure
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/rails_error_dashboard/services/n_plus_one_detector.rb', line 54 def normalize_sql(sql) normalized = sql.to_s.dup # Replace single-quoted string literals with ? normalized.gsub!(/'[^']*'/, "?") # Replace IN (...) contents with single ? normalized.gsub!(/\bIN\s*\([^)]+\)/i, "IN (?)") # Replace standalone numeric literals with ? # Negative lookbehind for " to avoid replacing inside double-quoted identifiers normalized.gsub!(/(?<!")(?<!\w)\d+(?!\w)(?!")/, "?") normalized rescue => e sql.to_s end |