rails_vitals 0.4.0 → 0.4.2

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/app/assets/stylesheets/rails_vitals/application.css +43 -0
  4. data/app/helpers/rails_vitals/application_helper.rb +23 -1
  5. data/app/views/rails_vitals/dashboard/index.html.erb +8 -16
  6. data/app/views/rails_vitals/explains/show.html.erb +9 -40
  7. data/app/views/rails_vitals/heatmap/index.html.erb +3 -4
  8. data/app/views/rails_vitals/models/index.html.erb +2 -3
  9. data/app/views/rails_vitals/n_plus_ones/index.html.erb +1 -3
  10. data/app/views/rails_vitals/n_plus_ones/show.html.erb +7 -8
  11. data/app/views/rails_vitals/playgrounds/index.html.erb +2 -0
  12. data/app/views/rails_vitals/requests/index.html.erb +6 -12
  13. data/app/views/rails_vitals/requests/show.html.erb +23 -25
  14. data/app/views/rails_vitals/shared/_empty_state.html.erb +3 -0
  15. data/app/views/rails_vitals/shared/_n1_indicator.html.erb +9 -0
  16. data/app/views/rails_vitals/shared/_page_header.html.erb +15 -0
  17. data/app/views/rails_vitals/shared/_score_badge.html.erb +3 -0
  18. data/lib/rails_vitals/analyzers/association_mapper.rb +1 -1
  19. data/lib/rails_vitals/analyzers/explain_analyzer.rb +59 -57
  20. data/lib/rails_vitals/analyzers/n_plus_one_aggregator.rb +13 -13
  21. data/lib/rails_vitals/analyzers/sql_tokenizer.rb +81 -81
  22. data/lib/rails_vitals/collector.rb +18 -18
  23. data/lib/rails_vitals/instrumentation/callback_instrumentation.rb +3 -3
  24. data/lib/rails_vitals/notifications/subscriber.rb +3 -3
  25. data/lib/rails_vitals/panel_renderer.rb +7 -11
  26. data/lib/rails_vitals/playground/sandbox.rb +2 -1
  27. data/lib/rails_vitals/request_record.rb +12 -12
  28. data/lib/rails_vitals/scorers/base_scorer.rb +3 -3
  29. data/lib/rails_vitals/scorers/composite_scorer.rb +3 -3
  30. data/lib/rails_vitals/scorers/query_scorer.rb +3 -3
  31. data/lib/rails_vitals/store.rb +2 -2
  32. data/lib/rails_vitals/version.rb +1 -1
  33. metadata +5 -1
@@ -13,104 +13,104 @@ module RailsVitals
13
13
  # Node types and their risk/education metadata
14
14
  NODE_METADATA = {
15
15
  "Seq Scan" => {
16
- risk: :danger,
17
- color: COLOR_DANGER,
18
- label: "Sequential Scan",
16
+ risk: :danger,
17
+ color: COLOR_DANGER,
18
+ label: "Sequential Scan",
19
19
  explanation: "PostgreSQL read every row in the table to find matches. " \
20
20
  "No index was used. This gets linearly slower as the table grows, " \
21
21
  "at 1M rows it scans 1M rows for every query hit.",
22
- fix_type: :missing_index
22
+ fix_type: :missing_index
23
23
  },
24
24
  "Index Scan" => {
25
- risk: :healthy,
26
- color: COLOR_HEALTHY,
27
- label: "Index Scan",
25
+ risk: :healthy,
26
+ color: COLOR_HEALTHY,
27
+ label: "Index Scan",
28
28
  explanation: "PostgreSQL used an index to locate matching rows directly. " \
29
29
  "Fast and consistent regardless of table size.",
30
- fix_type: nil
30
+ fix_type: nil
31
31
  },
32
32
  "Index Only Scan" => {
33
- risk: :healthy,
34
- color: COLOR_HEALTHY,
35
- label: "Index Only Scan",
33
+ risk: :healthy,
34
+ color: COLOR_HEALTHY,
35
+ label: "Index Only Scan",
36
36
  explanation: "PostgreSQL satisfied the query entirely from the index " \
37
37
  "without touching the table. The most efficient scan type.",
38
- fix_type: nil
38
+ fix_type: nil
39
39
  },
40
40
  "Bitmap Heap Scan" => {
41
- risk: :neutral,
42
- color: COLOR_WARNING,
43
- label: "Bitmap Heap Scan",
41
+ risk: :neutral,
42
+ color: COLOR_WARNING,
43
+ label: "Bitmap Heap Scan",
44
44
  explanation: "PostgreSQL built a bitmap of matching index entries, then " \
45
45
  "fetched the actual rows. Common with IN (...) conditions and " \
46
46
  "multiple indexes. Generally acceptable.",
47
- fix_type: nil
47
+ fix_type: nil
48
48
  },
49
49
  "Bitmap Index Scan" => {
50
- risk: :neutral,
51
- color: COLOR_WARNING,
52
- label: "Bitmap Index Scan",
50
+ risk: :neutral,
51
+ color: COLOR_WARNING,
52
+ label: "Bitmap Index Scan",
53
53
  explanation: "Builds a bitmap of row locations from an index. " \
54
54
  "Works in conjunction with Bitmap Heap Scan.",
55
- fix_type: nil
55
+ fix_type: nil
56
56
  },
57
57
  "Hash Join" => {
58
- risk: :neutral,
59
- color: COLOR_INFO,
60
- label: "Hash Join",
58
+ risk: :neutral,
59
+ color: COLOR_INFO,
60
+ label: "Hash Join",
61
61
  explanation: "Builds a hash table from one side of the JOIN, then probes " \
62
62
  "it for each row from the other side. Common with includes() " \
63
63
  "and eager_load(). Efficient for large datasets.",
64
- fix_type: nil
64
+ fix_type: nil
65
65
  },
66
66
  "Nested Loop" => {
67
- risk: :warning,
68
- color: COLOR_WARNING,
69
- label: "Nested Loop",
67
+ risk: :warning,
68
+ color: COLOR_WARNING,
69
+ label: "Nested Loop",
70
70
  explanation: "For each row on the outer side, scans the inner side. " \
71
71
  "Fast when the inner side is small or uses an index. " \
72
72
  "Dangerous when both sides are large, O(n²) complexity.",
73
- fix_type: :check_join_indexes
73
+ fix_type: :check_join_indexes
74
74
  },
75
75
  "Merge Join" => {
76
- risk: :neutral,
77
- color: COLOR_COOL,
78
- label: "Merge Join",
76
+ risk: :neutral,
77
+ color: COLOR_COOL,
78
+ label: "Merge Join",
79
79
  explanation: "Joins two pre-sorted datasets by merging them in order. " \
80
80
  "Efficient when both sides are already sorted on the join key.",
81
- fix_type: nil
81
+ fix_type: nil
82
82
  },
83
83
  "Aggregate" => {
84
- risk: :neutral,
85
- color: COLOR_NEUTRAL,
86
- label: "Aggregate",
84
+ risk: :neutral,
85
+ color: COLOR_NEUTRAL,
86
+ label: "Aggregate",
87
87
  explanation: "Computes an aggregate function (COUNT, SUM, AVG, etc.) " \
88
88
  "over a set of rows. Generated by .count, .sum, .average in Rails.",
89
- fix_type: nil
89
+ fix_type: nil
90
90
  },
91
91
  "Hash" => {
92
- risk: :neutral,
93
- color: COLOR_NEUTRAL,
94
- label: "Hash",
92
+ risk: :neutral,
93
+ color: COLOR_NEUTRAL,
94
+ label: "Hash",
95
95
  explanation: "Builds a hash table in memory for use by a Hash Join node above it.",
96
- fix_type: nil
96
+ fix_type: nil
97
97
  },
98
98
  "Sort" => {
99
- risk: :warning,
100
- color: COLOR_WARNING,
101
- label: "Sort",
99
+ risk: :warning,
100
+ color: COLOR_WARNING,
101
+ label: "Sort",
102
102
  explanation: "PostgreSQL sorted rows in memory (or on disk if large). " \
103
103
  "Generated by ORDER BY on an unindexed column. " \
104
104
  "An index on the sort column eliminates this node entirely.",
105
- fix_type: :index_sort_column
105
+ fix_type: :index_sort_column
106
106
  },
107
107
  "Limit" => {
108
- risk: :healthy,
109
- color: COLOR_HEALTHY,
110
- label: "Limit",
108
+ risk: :healthy,
109
+ color: COLOR_HEALTHY,
110
+ label: "Limit",
111
111
  explanation: "Stops fetching rows once the LIMIT is reached. " \
112
112
  "Good, always paginate large result sets.",
113
- fix_type: nil
113
+ fix_type: nil
114
114
  }
115
115
  }.freeze
116
116
 
@@ -147,18 +147,18 @@ module RailsVitals
147
147
  exec_time = plan_json.first["Execution Time"]&.round(2)
148
148
 
149
149
  root_node = build_node(root_plan)
150
- warnings = extract_warnings(root_node)
150
+ warnings = extract_warnings(root_node)
151
151
  suggestions = build_suggestions(warnings, root_node)
152
152
 
153
153
  result = Result.new(
154
- sql: safe_sql,
155
- plan: root_node,
156
- warnings: warnings,
157
- suggestions: suggestions,
158
- total_cost: root_plan["Total Cost"]&.round(2),
154
+ sql: safe_sql,
155
+ plan: root_node,
156
+ warnings: warnings,
157
+ suggestions: suggestions,
158
+ total_cost: root_plan["Total Cost"]&.round(2),
159
159
  actual_time_ms: exec_time,
160
- rows_examined: count_rows_examined(root_plan),
161
- error: nil
160
+ rows_examined: count_rows_examined(root_plan),
161
+ error: nil
162
162
  )
163
163
 
164
164
  result.interpretation = interpret(result)
@@ -186,6 +186,7 @@ module RailsVitals
186
186
  value = bind.is_a?(String) ? "'#{bind.gsub("'", "''")}'" : bind.to_s
187
187
  result = result.gsub("$#{i + 1}", value)
188
188
  end
189
+
189
190
  # Replace any remaining ? placeholders with NULL
190
191
  result.gsub("?", "NULL")
191
192
  end
@@ -263,7 +264,7 @@ module RailsVitals
263
264
  migration: fk ?
264
265
  "add_index :#{w[:table]}, :#{fk}" :
265
266
  "add_index :#{w[:table]}, :COLUMN_NAME",
266
- command: "rails g migration Add#{fk&.camelize || 'Index'}To#{w[:table].camelize}"
267
+ command: "rails g migration Add#{fk&.camelize || 'Index'}To#{w[:table].camelize}"
267
268
  }
268
269
  when :sort_without_index
269
270
  suggestions << {
@@ -291,6 +292,7 @@ module RailsVitals
291
292
 
292
293
  def self.extract_fk_from_filter(filter)
293
294
  return nil unless filter
295
+
294
296
  match = filter.match(/\((\w+)\s*=\s*/i)
295
297
  match ? match[1] : nil
296
298
  end
@@ -4,11 +4,11 @@ module RailsVitals
4
4
  def self.aggregate(records)
5
5
  pattern_data = Hash.new do |h, k|
6
6
  h[k] = {
7
- pattern: k,
8
- occurrences: 0,
9
- endpoints: Hash.new(0),
10
- table: nil,
11
- foreign_key: nil
7
+ pattern: k,
8
+ occurrences: 0,
9
+ endpoints: Hash.new(0),
10
+ table: nil,
11
+ foreign_key: nil
12
12
  }
13
13
  end
14
14
 
@@ -17,11 +17,11 @@ module RailsVitals
17
17
 
18
18
  record.n_plus_one_patterns.each do |sql, count|
19
19
  normalized = normalize(sql)
20
- Rails.logger.debug "Processing SQL: #{sql} → normalized: #{normalized}"
20
+ Rails.logger.debug "#{self.name}: Processing SQL: #{sql} → normalized: #{normalized}"
21
21
 
22
22
  pattern_data[normalized][:occurrences] += count
23
23
  pattern_data[normalized][:endpoints][record.endpoint] += 1
24
- pattern_data[normalized][:table] ||= extract_table(sql)
24
+ pattern_data[normalized][:table] ||= extract_table(sql)
25
25
  pattern_data[normalized][:foreign_key] ||= extract_foreign_key(sql)
26
26
  end
27
27
  end
@@ -39,7 +39,7 @@ module RailsVitals
39
39
 
40
40
  def self.normalize(sql)
41
41
  sql
42
- .gsub('\\"', '"') # unescape stored escaped quotes
42
+ .gsub('\\"', '"') # unescape stored escaped quotes
43
43
  .gsub(/\b\d+\b/, "?")
44
44
  .gsub(/'[^']*'/, "?")
45
45
  .gsub(/\s+/, " ")
@@ -57,7 +57,7 @@ module RailsVitals
57
57
  end
58
58
 
59
59
  def self.build_suggestion(pattern)
60
- table = pattern[:table]
60
+ table = pattern[:table]
61
61
  foreign_key = pattern[:foreign_key]
62
62
 
63
63
  return generic_suggestion(table) unless table && foreign_key
@@ -68,9 +68,9 @@ module RailsVitals
68
68
 
69
69
  if owner_model && assoc_name
70
70
  {
71
- code: "#{owner_model}.includes(:#{assoc_name})",
71
+ code: "#{owner_model}.includes(:#{assoc_name})",
72
72
  description: "Eager load :#{assoc_name} on #{owner_model} to eliminate this N+1",
73
- owner: owner_model,
73
+ owner: owner_model,
74
74
  association: assoc_name
75
75
  }
76
76
  else
@@ -105,9 +105,9 @@ module RailsVitals
105
105
 
106
106
  def self.generic_suggestion(table)
107
107
  {
108
- code: "includes(:#{table&.singularize})",
108
+ code: "includes(:#{table&.singularize})",
109
109
  description: "Use includes(), eager_load(), or preload() to batch this query",
110
- owner: nil,
110
+ owner: nil,
111
111
  association: table&.singularize
112
112
  }
113
113
  end
@@ -3,63 +3,63 @@ module RailsVitals
3
3
  class SqlTokenizer
4
4
  TOKEN_DEFINITIONS = [
5
5
  {
6
- type: :select_star,
7
- pattern: /\bSELECT\s+\*\b/i,
8
- label: "SELECT *",
9
- color: "#4299e1",
10
- risk: :warning,
6
+ type: :select_star,
7
+ pattern: /\bSELECT\s+\*\b/i,
8
+ label: "SELECT *",
9
+ color: "#4299e1",
10
+ risk: :warning,
11
11
  explanation: "Fetches all columns from the table. In Rails this is the default " \
12
12
  "behavior of Model.all and most queries. Can be wasteful when you " \
13
13
  "only need specific attributes. Use .select(:id, :name) to fetch " \
14
14
  "only what you need, especially on wide tables."
15
15
  },
16
16
  {
17
- type: :select,
18
- pattern: /\bSELECT\b(?!\s+\*)/i,
19
- label: "SELECT",
20
- color: "#4299e1",
21
- risk: :healthy,
17
+ type: :select,
18
+ pattern: /\bSELECT\b(?!\s+\*)/i,
19
+ label: "SELECT",
20
+ color: "#4299e1",
21
+ risk: :healthy,
22
22
  explanation: "Fetches specific columns. More efficient than SELECT * when your " \
23
23
  "table has many columns or large text/json fields you don't need."
24
24
  },
25
25
  {
26
- type: :count,
27
- pattern: /\bCOUNT\s*\(/i,
28
- label: "COUNT(*)",
29
- color: "#ed8936",
30
- risk: :warning,
26
+ type: :count,
27
+ pattern: /\bCOUNT\s*\(/i,
28
+ label: "COUNT(*)",
29
+ color: "#ed8936",
30
+ risk: :warning,
31
31
  explanation: "Counts rows matching the condition. When this appears in a loop " \
32
32
  "(N+1 pattern), Rails fires one COUNT query per record. The fix is " \
33
33
  "a counter cache column or loading the association and calling .size " \
34
34
  "which uses the already-loaded records instead of hitting the DB."
35
35
  },
36
36
  {
37
- type: :aggregation,
38
- pattern: /\b(SUM|AVG|MIN|MAX)\s*\(/i,
39
- label: "AGGREGATE",
40
- color: "#ed8936",
41
- risk: :warning,
37
+ type: :aggregation,
38
+ pattern: /\b(SUM|AVG|MIN|MAX)\s*\(/i,
39
+ label: "AGGREGATE",
40
+ color: "#ed8936",
41
+ risk: :warning,
42
42
  explanation: "Aggregation function (SUM/AVG/MIN/MAX). Like COUNT, these are " \
43
43
  "dangerous in loops. Each call fires a separate query. Consider " \
44
44
  "loading the association once and using Ruby's .sum/.min/.max on " \
45
45
  "the already-loaded collection instead."
46
46
  },
47
47
  {
48
- type: :from,
49
- pattern: /\bFROM\s+"?(\w+)"?/i,
50
- label: "FROM",
51
- color: "#68d391",
52
- risk: :healthy,
48
+ type: :from,
49
+ pattern: /\bFROM\s+"?(\w+)"?/i,
50
+ label: "FROM",
51
+ color: "#68d391",
52
+ risk: :healthy,
53
53
  explanation: "Identifies which table (and therefore which ActiveRecord model) " \
54
54
  "is being queried. In an N+1, you'll see the same FROM table " \
55
55
  "repeated many times, once per parent record."
56
56
  },
57
57
  {
58
- type: :where_fk,
59
- pattern: /\bWHERE\s+.*\w+_id\s*=/i,
60
- label: "WHERE fk =",
61
- color: "#fc8181",
62
- risk: :danger,
58
+ type: :where_fk,
59
+ pattern: /\bWHERE\s+.*\w+_id\s*=/i,
60
+ label: "WHERE fk =",
61
+ color: "#fc8181",
62
+ risk: :danger,
63
63
  explanation: "WHERE condition on a foreign key with a single value. This is " \
64
64
  "the N+1 signature, loading one associated record at a time. " \
65
65
  "When you see this pattern repeated, it means Rails is fetching " \
@@ -67,33 +67,33 @@ module RailsVitals
67
67
  "which replaces this with a single WHERE fk IN (...) query."
68
68
  },
69
69
  {
70
- type: :where,
71
- pattern: /\bWHERE\b/i,
72
- label: "WHERE",
73
- color: "#f6ad55",
74
- risk: :neutral,
70
+ type: :where,
71
+ pattern: /\bWHERE\b/i,
72
+ label: "WHERE",
73
+ color: "#f6ad55",
74
+ risk: :neutral,
75
75
  explanation: "Filters rows by condition. Efficient when the condition column " \
76
76
  "has an index. Slow when it doesn't. DB engine will scan every " \
77
77
  "row in the table (Sequential Scan). Check the EXPLAIN output to " \
78
78
  "verify an index is being used."
79
79
  },
80
80
  {
81
- type: :where_in,
82
- pattern: /\bIN\s*\(/i,
83
- label: "IN (...)",
84
- color: "#68d391",
85
- risk: :healthy,
81
+ type: :where_in,
82
+ pattern: /\bIN\s*\(/i,
83
+ label: "IN (...)",
84
+ color: "#68d391",
85
+ risk: :healthy,
86
86
  explanation: "Batch lookup fetches multiple records in one query using a list " \
87
87
  "of values. This is what eager loading (includes/preload) generates " \
88
88
  "instead of repeated WHERE fk = ? queries. Seeing IN (...) means " \
89
89
  "your associations are being loaded efficiently."
90
90
  },
91
91
  {
92
- type: :inner_join,
93
- pattern: /\bINNER\s+JOIN\b/i,
94
- label: "INNER JOIN",
95
- color: "#9f7aea",
96
- risk: :neutral,
92
+ type: :inner_join,
93
+ pattern: /\bINNER\s+JOIN\b/i,
94
+ label: "INNER JOIN",
95
+ color: "#9f7aea",
96
+ risk: :neutral,
97
97
  explanation: "Combines rows from two tables where the join condition matches. " \
98
98
  "In Rails this is what .joins() generates. Records without a " \
99
99
  "matching association are excluded from results. Note: .joins() " \
@@ -101,22 +101,22 @@ module RailsVitals
101
101
  "to access associated data."
102
102
  },
103
103
  {
104
- type: :left_join,
105
- pattern: /\bLEFT\s+(OUTER\s+)?JOIN\b/i,
106
- label: "LEFT JOIN",
107
- color: "#9f7aea",
108
- risk: :neutral,
104
+ type: :left_join,
105
+ pattern: /\bLEFT\s+(OUTER\s+)?JOIN\b/i,
106
+ label: "LEFT JOIN",
107
+ color: "#9f7aea",
108
+ risk: :neutral,
109
109
  explanation: "Like INNER JOIN but keeps all rows from the left table even " \
110
110
  "when there's no matching row on the right. In Rails this is " \
111
111
  "what .eager_load() and .left_joins() generate. Use when you " \
112
112
  "need to include records that have no associated data."
113
113
  },
114
114
  {
115
- type: :order,
116
- pattern: /\bORDER\s+BY\b/i,
117
- label: "ORDER BY",
118
- color: "#76e4f7",
119
- risk: :warning,
115
+ type: :order,
116
+ pattern: /\bORDER\s+BY\b/i,
117
+ label: "ORDER BY",
118
+ color: "#76e4f7",
119
+ risk: :warning,
120
120
  explanation: "Sorts results by a column. Fast when sorting on an indexed " \
121
121
  "column. Slow when sorting on an unindexed column. DB engine " \
122
122
  "must sort all matching rows in memory. Default Rails scopes " \
@@ -124,22 +124,22 @@ module RailsVitals
124
124
  "has an index if your table is large."
125
125
  },
126
126
  {
127
- type: :limit,
128
- pattern: /\bLIMIT\s+\d+/i,
129
- label: "LIMIT",
130
- color: "#a0aec0",
131
- risk: :healthy,
127
+ type: :limit,
128
+ pattern: /\bLIMIT\s+\d+/i,
129
+ label: "LIMIT",
130
+ color: "#a0aec0",
131
+ risk: :healthy,
132
132
  explanation: "Restricts the number of rows returned. Always use LIMIT in " \
133
133
  "production feeds and lists, never load unbounded data. " \
134
134
  "Note: LIMIT with OFFSET becomes slower as OFFSET grows " \
135
135
  "because DB engine must scan and discard all preceding rows."
136
136
  },
137
137
  {
138
- type: :offset,
139
- pattern: /\bOFFSET\s+\d+/i,
140
- label: "OFFSET",
141
- color: "#fc8181",
142
- risk: :warning,
138
+ type: :offset,
139
+ pattern: /\bOFFSET\s+\d+/i,
140
+ label: "OFFSET",
141
+ color: "#fc8181",
142
+ risk: :warning,
143
143
  explanation: "Skips N rows before returning results. Common in pagination " \
144
144
  "(page 2, page 3...). The hidden cost: DB engine must read " \
145
145
  "and discard all rows before the offset, at page 100 with " \
@@ -147,11 +147,11 @@ module RailsVitals
147
147
  "cursor-based pagination (WHERE id > last_id) for large datasets."
148
148
  },
149
149
  {
150
- type: :group_by,
151
- pattern: /\bGROUP\s+BY\b/i,
152
- label: "GROUP BY",
153
- color: "#76e4f7",
154
- risk: :neutral,
150
+ type: :group_by,
151
+ pattern: /\bGROUP\s+BY\b/i,
152
+ label: "GROUP BY",
153
+ color: "#76e4f7",
154
+ risk: :neutral,
155
155
  explanation: "Groups rows sharing a value and applies aggregate functions " \
156
156
  "per group. Common with COUNT, SUM, AVG. Used in Rails with " \
157
157
  ".group(:column). Consider a counter cache column if you're " \
@@ -162,11 +162,11 @@ module RailsVitals
162
162
 
163
163
  COMPLEXITY_RULES = [
164
164
  { tokens: [ :left_join, :inner_join ], points: 2 },
165
- { tokens: [ :where_fk ], points: 3 },
166
- { tokens: [ :count, :aggregation ], points: 2 },
167
- { tokens: [ :offset ], points: 2 },
168
- { tokens: [ :group_by ], points: 1 },
169
- { tokens: [ :order ], points: 1 }
165
+ { tokens: [ :where_fk ], points: 3 },
166
+ { tokens: [ :count, :aggregation ], points: 2 },
167
+ { tokens: [ :offset ], points: 2 },
168
+ { tokens: [ :group_by ], points: 1 },
169
+ { tokens: [ :order ], points: 1 }
170
170
  ].freeze
171
171
 
172
172
  Result = Struct.new(:tokens, :complexity, :complexity_label,
@@ -181,12 +181,12 @@ module RailsVitals
181
181
  repetition = calculate_repetition(sql, all_queries)
182
182
 
183
183
  Result.new(
184
- tokens: matched,
185
- complexity: complexity,
184
+ tokens: matched,
185
+ complexity: complexity,
186
186
  complexity_label: complexity_label(complexity),
187
- risk: risk,
187
+ risk: risk,
188
188
  repetition_count: repetition,
189
- repetition_bar: repetition_bar(repetition, all_queries.size)
189
+ repetition_bar: repetition_bar(repetition, all_queries.size)
190
190
  )
191
191
  end
192
192
 
@@ -203,9 +203,9 @@ module RailsVitals
203
203
 
204
204
  def self.complexity_label(score)
205
205
  case score
206
- when 1..2 then { label: "Simple", color: "#68d391" }
206
+ when 1..2 then { label: "Simple", color: "#68d391" }
207
207
  when 3..5 then { label: "Moderate", color: "#f6ad55" }
208
- else { label: "Complex", color: "#fc8181" }
208
+ else { label: "Complex", color: "#fc8181" }
209
209
  end
210
210
  end
211
211
 
@@ -4,43 +4,43 @@ module RailsVitals
4
4
  :response_status, :duration_ms, :started_at
5
5
 
6
6
  def initialize
7
- @queries = []
8
- @callbacks = []
9
- @controller = nil
10
- @action = nil
11
- @http_method = nil
7
+ @queries = []
8
+ @callbacks = []
9
+ @controller = nil
10
+ @action = nil
11
+ @http_method = nil
12
12
  @response_status = nil
13
- @duration_ms = nil
14
- @started_at = Time.now
13
+ @duration_ms = nil
14
+ @started_at = Time.now
15
15
  end
16
16
 
17
17
  # Called by the sql.active_record subscriber
18
18
  def add_query(sql:, duration_ms:, source:, binds: [])
19
19
  @queries << {
20
- sql: sql,
20
+ sql: sql,
21
21
  duration_ms: duration_ms,
22
- source: source,
23
- binds: binds,
24
- called_at: Time.now
22
+ source: source,
23
+ binds: binds,
24
+ called_at: Time.now
25
25
  }
26
26
  end
27
27
 
28
28
  def add_callback(model:, kind:, duration_ms:)
29
29
  @callbacks << {
30
- model: model,
31
- kind: kind,
30
+ model: model,
31
+ kind: kind,
32
32
  duration_ms: duration_ms,
33
- called_at: Time.now
33
+ called_at: Time.now
34
34
  }
35
35
  end
36
36
 
37
37
  # Called by the process_action.action_controller subscriber
38
38
  def finalize!(event)
39
- @controller = event.payload[:controller]
40
- @action = event.payload[:action]
41
- @http_method = event.payload[:method]
39
+ @controller = event.payload[:controller]
40
+ @action = event.payload[:action]
41
+ @http_method = event.payload[:method]
42
42
  @response_status = event.payload[:status]
43
- @duration_ms = event.duration
43
+ @duration_ms = event.duration
44
44
  end
45
45
 
46
46
  def total_query_count
@@ -13,13 +13,13 @@ module RailsVitals
13
13
  return super
14
14
  end
15
15
 
16
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
16
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
17
17
  result = super
18
18
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
19
19
 
20
20
  collector.add_callback(
21
- model: self.class.name,
22
- kind: kind,
21
+ model: self.class.name,
22
+ kind: kind,
23
23
  duration_ms: duration.round(2)
24
24
  )
25
25
 
@@ -28,10 +28,10 @@ module RailsVitals
28
28
  next if rails_vitals_request?
29
29
 
30
30
  collector.add_query(
31
- sql: event.payload[:sql],
31
+ sql: event.payload[:sql],
32
32
  duration_ms: event.duration,
33
- source: extract_source(event.payload[:binds]),
34
- binds: event.payload[:binds]&.map(&:value) || []
33
+ source: extract_source(event.payload[:binds]),
34
+ binds: event.payload[:binds]&.map(&:value) || []
35
35
  )
36
36
  end
37
37
  end