rails_vitals 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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +340 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/rails_vitals/application.css +180 -0
  6. data/app/controllers/rails_vitals/application_controller.rb +30 -0
  7. data/app/controllers/rails_vitals/associations_controller.rb +8 -0
  8. data/app/controllers/rails_vitals/dashboard_controller.rb +59 -0
  9. data/app/controllers/rails_vitals/heatmap_controller.rb +39 -0
  10. data/app/controllers/rails_vitals/models_controller.rb +65 -0
  11. data/app/controllers/rails_vitals/n_plus_ones_controller.rb +43 -0
  12. data/app/controllers/rails_vitals/requests_controller.rb +44 -0
  13. data/app/helpers/rails_vitals/application_helper.rb +63 -0
  14. data/app/jobs/rails_vitals/application_job.rb +4 -0
  15. data/app/mailers/rails_vitals/application_mailer.rb +6 -0
  16. data/app/models/rails_vitals/application_record.rb +5 -0
  17. data/app/views/layouts/rails_vitals/application.html.erb +27 -0
  18. data/app/views/rails_vitals/associations/index.html.erb +370 -0
  19. data/app/views/rails_vitals/dashboard/index.html.erb +158 -0
  20. data/app/views/rails_vitals/heatmap/index.html.erb +66 -0
  21. data/app/views/rails_vitals/models/index.html.erb +117 -0
  22. data/app/views/rails_vitals/n_plus_ones/index.html.erb +49 -0
  23. data/app/views/rails_vitals/n_plus_ones/show.html.erb +139 -0
  24. data/app/views/rails_vitals/requests/index.html.erb +60 -0
  25. data/app/views/rails_vitals/requests/show.html.erb +396 -0
  26. data/config/routes.rb +9 -0
  27. data/lib/rails_vitals/analyzers/association_mapper.rb +121 -0
  28. data/lib/rails_vitals/analyzers/n_plus_one_aggregator.rb +116 -0
  29. data/lib/rails_vitals/analyzers/sql_tokenizer.rb +240 -0
  30. data/lib/rails_vitals/collector.rb +78 -0
  31. data/lib/rails_vitals/configuration.rb +27 -0
  32. data/lib/rails_vitals/engine.rb +25 -0
  33. data/lib/rails_vitals/instrumentation/callback_instrumentation.rb +30 -0
  34. data/lib/rails_vitals/middleware/panel_injector.rb +75 -0
  35. data/lib/rails_vitals/notifications/subscriber.rb +59 -0
  36. data/lib/rails_vitals/panel_renderer.rb +233 -0
  37. data/lib/rails_vitals/request_record.rb +51 -0
  38. data/lib/rails_vitals/scorers/base_scorer.rb +25 -0
  39. data/lib/rails_vitals/scorers/composite_scorer.rb +36 -0
  40. data/lib/rails_vitals/scorers/n_plus_one_scorer.rb +43 -0
  41. data/lib/rails_vitals/scorers/query_scorer.rb +42 -0
  42. data/lib/rails_vitals/store.rb +34 -0
  43. data/lib/rails_vitals/version.rb +3 -0
  44. data/lib/rails_vitals.rb +33 -0
  45. data/lib/tasks/rails_vitals_tasks.rake +4 -0
  46. metadata +113 -0
@@ -0,0 +1,233 @@
1
+ # lib/rails_vitals/panel_renderer.rb
2
+ module RailsVitals
3
+ class PanelRenderer
4
+ def self.render(collector, scorer)
5
+ new(collector, scorer).render
6
+ end
7
+
8
+ def initialize(collector, scorer)
9
+ @collector = collector
10
+ @scorer = scorer
11
+ end
12
+
13
+ def render
14
+ <<~HTML
15
+ <div id="rails-vitals-panel" style="#{panel_styles}">
16
+ #{toggle_button}
17
+ #{collapsed_badge}
18
+ #{expanded_content}
19
+ </div>
20
+ #{inline_script}
21
+ HTML
22
+ end
23
+
24
+ private
25
+
26
+ # ─── PANEL WRAPPER ───────────────────────────────────────────
27
+
28
+ def panel_styles
29
+ "position:fixed;bottom:20px;right:20px;z-index:999999;font-family:monospace;font-size:12px;"
30
+ end
31
+
32
+ # ─── COLLAPSED STATE ─────────────────────────────────────────
33
+
34
+ def collapsed_badge
35
+ <<~HTML
36
+ <div id="rv-badge" style="
37
+ background:#{score_bg};
38
+ color:#fff;
39
+ padding:6px 12px;
40
+ border-radius:6px;
41
+ cursor:pointer;
42
+ display:flex;
43
+ align-items:center;
44
+ gap:8px;
45
+ box-shadow:0 2px 8px rgba(0,0,0,0.3);
46
+ " onclick="rvToggle()">
47
+ <span style="font-weight:bold;font-size:14px;">#{@scorer.score}</span>
48
+ <span style="opacity:0.85;">#{@scorer.label}</span>
49
+ <span style="opacity:0.6;font-size:10px;">▲</span>
50
+ </div>
51
+ HTML
52
+ end
53
+
54
+ def toggle_button; ""; end
55
+
56
+ # ─── EXPANDED STATE ───────────────────────────────────────────
57
+
58
+ def expanded_content
59
+ <<~HTML
60
+ <div id="rv-expanded" style="
61
+ display:none;
62
+ background:#1a1a2e;
63
+ color:#e2e8f0;
64
+ border-radius:8px;
65
+ padding:16px;
66
+ margin-bottom:8px;
67
+ width:380px;
68
+ box-shadow:0 4px 20px rgba(0,0,0,0.5);
69
+ border:1px solid #2d3748;
70
+ ">
71
+ #{header_section}
72
+ #{divider}
73
+ #{request_info_section}
74
+ #{divider}
75
+ #{query_summary_section}
76
+ #{divider}
77
+ #{slowest_queries_section}
78
+ #{divider}
79
+ #{admin_link_section}
80
+ </div>
81
+ HTML
82
+ end
83
+
84
+ # ─── SECTIONS ────────────────────────────────────────────────
85
+
86
+ def header_section
87
+ <<~HTML
88
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
89
+ <div>
90
+ <span style="font-size:28px;font-weight:bold;color:#{score_bg};">#{@scorer.score}</span>
91
+ <span style="font-size:13px;color:#a0aec0;margin-left:6px;">/ 100</span>
92
+ </div>
93
+ <div style="text-align:right;">
94
+ <div style="
95
+ background:#{score_bg};
96
+ color:#fff;
97
+ padding:3px 10px;
98
+ border-radius:4px;
99
+ font-size:11px;
100
+ font-weight:bold;
101
+ ">#{@scorer.label}</div>
102
+ </div>
103
+ </div>
104
+ HTML
105
+ end
106
+
107
+ def request_info_section
108
+ <<~HTML
109
+ <div style="margin-bottom:4px;">
110
+ #{label_row("Endpoint", "#{@collector.controller}##{@collector.action}")}
111
+ #{label_row("Method", @collector.http_method.to_s.upcase)}
112
+ #{label_row("Status", @collector.response_status.to_s)}
113
+ #{label_row("Duration", "#{@collector.duration_ms&.round(1)}ms")}
114
+ </div>
115
+ HTML
116
+ end
117
+
118
+ def query_summary_section
119
+ n_plus_one_count = n_plus_one_scorer.n_plus_one_patterns.size
120
+
121
+ <<~HTML
122
+ <div style="margin-bottom:4px;">
123
+ #{label_row("Queries", @collector.total_query_count.to_s)}
124
+ #{label_row("DB Time", "#{@collector.total_db_time_ms.round(1)}ms")}
125
+ #{label_row("N+1", n_plus_one_badge(n_plus_one_count))}
126
+ </div>
127
+ HTML
128
+ end
129
+
130
+ def slowest_queries_section
131
+ queries = @collector.slowest_queries(5)
132
+ return "" if queries.empty?
133
+
134
+ rows = queries.map do |q|
135
+ sql = truncate(q[:sql], 45)
136
+ time_ms = q[:duration_ms].round(1)
137
+ <<~HTML
138
+ <div style="margin-bottom:6px;">
139
+ <div style="color:#90cdf4;font-size:11px;">#{escape(sql)}</div>
140
+ <div style="color:#68d391;font-size:10px;">#{time_ms}ms</div>
141
+ </div>
142
+ HTML
143
+ end
144
+
145
+ <<~HTML
146
+ <div>
147
+ <div style="color:#a0aec0;font-size:10px;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em;">
148
+ Slowest Queries
149
+ </div>
150
+ #{rows.join}
151
+ </div>
152
+ HTML
153
+ end
154
+
155
+ def admin_link_section
156
+ record = RailsVitals.store.all.last
157
+ return "" unless record
158
+
159
+ <<~HTML
160
+ <div style="text-align:right;margin-top:4px;">
161
+ <a href="/rails_vitals/requests/#{record.id}"
162
+ style="color:#90cdf4;font-size:11px;text-decoration:none;"
163
+ target="_blank">
164
+ View full report →
165
+ </a>
166
+ </div>
167
+ HTML
168
+ end
169
+
170
+ # ─── HELPERS ─────────────────────────────────────────────────
171
+
172
+ def divider
173
+ "<div style='border-top:1px solid #2d3748;margin:10px 0;'></div>"
174
+ end
175
+
176
+ def label_row(label, value)
177
+ <<~HTML
178
+ <div style="display:flex;justify-content:space-between;padding:2px 0;">
179
+ <span style="color:#a0aec0;">#{label}</span>
180
+ <span style="color:#e2e8f0;">#{value}</span>
181
+ </div>
182
+ HTML
183
+ end
184
+
185
+ def n_plus_one_badge(count)
186
+ return "<span style='color:#68d391;'>None</span>" if count.zero?
187
+
188
+ "<span style='background:#e53e3e;color:#fff;padding:1px 6px;border-radius:3px;font-size:10px;'>#{count} detected</span>"
189
+ end
190
+
191
+ def score_bg
192
+ case @scorer.color
193
+ when "green" then "#276749"
194
+ when "blue" then "#2b6cb0"
195
+ when "amber" then "#b7791f"
196
+ else "#c53030"
197
+ end
198
+ end
199
+
200
+ def n_plus_one_scorer
201
+ @n_plus_one_scorer ||= Scorers::NPlusOneScorer.new(@collector)
202
+ end
203
+
204
+ def truncate(str, length)
205
+ str.length > length ? "#{str[0, length]}…" : str
206
+ end
207
+
208
+ def escape(str)
209
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
210
+ end
211
+
212
+ # ─── JAVASCRIPT ──────────────────────────────────────────────
213
+
214
+ def inline_script
215
+ <<~HTML
216
+ <script>
217
+ function rvToggle() {
218
+ var expanded = document.getElementById('rv-expanded');
219
+ var badge = document.getElementById('rv-badge');
220
+ var arrow = badge.querySelector('span:last-child');
221
+ if (expanded.style.display === 'none') {
222
+ expanded.style.display = 'block';
223
+ arrow.textContent = '▼';
224
+ } else {
225
+ expanded.style.display = 'none';
226
+ arrow.textContent = '▲';
227
+ }
228
+ }
229
+ </script>
230
+ HTML
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,51 @@
1
+ module RailsVitals
2
+ class RequestRecord
3
+ attr_reader :id, :controller, :action, :http_method,
4
+ :response_status, :duration_ms, :score,
5
+ :label, :color, :queries, :n_plus_one_patterns,
6
+ :callbacks, :total_callback_time_ms, :recorded_at
7
+
8
+ def initialize(collector:, scorer:)
9
+ @id = SecureRandom.hex(8)
10
+ @controller = collector.controller
11
+ @action = collector.action
12
+ @http_method = collector.http_method
13
+ @response_status = collector.response_status
14
+ @duration_ms = collector.duration_ms
15
+ @queries = collector.queries
16
+ @callbacks = collector.callbacks
17
+ @total_callback_time_ms = collector.total_callback_time_ms
18
+ @score = scorer.score
19
+ @label = scorer.label
20
+ @color = scorer.color
21
+ @n_plus_one_patterns = build_n_plus_one_patterns(scorer)
22
+ @recorded_at = Time.now
23
+ end
24
+
25
+ def endpoint
26
+ "#{@controller}##{@action}"
27
+ end
28
+
29
+ def total_query_count
30
+ @queries.size
31
+ end
32
+
33
+ def total_db_time_ms
34
+ @queries.sum { |q| q[:duration_ms] }
35
+ end
36
+
37
+ def slowest_queries(limit = 3)
38
+ @queries.sort_by { |q| -q[:duration_ms] }.first(limit)
39
+ end
40
+
41
+ private
42
+
43
+ def build_n_plus_one_patterns(scorer)
44
+ scorer_instance = scorer.is_a?(Scorers::CompositeScorer) ?
45
+ Scorers::NPlusOneScorer.new(scorer.instance_variable_get(:@collector)) :
46
+ nil
47
+
48
+ scorer_instance&.n_plus_one_patterns || {}
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ module RailsVitals
2
+ module Scorers
3
+ class BaseScorer
4
+ HEALTHY = (90..100)
5
+ ACCEPTABLE = (70..89)
6
+ WARNING = (50..69)
7
+ CRITICAL = (0..49)
8
+
9
+ def initialize(collector)
10
+ @collector = collector
11
+ end
12
+
13
+ # Returns a score between 0 and 100
14
+ def score
15
+ raise NotImplementedError, "#{self.class} must implement #score"
16
+ end
17
+
18
+ private
19
+
20
+ def clamp(value)
21
+ value.clamp(0, 100)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ module RailsVitals
2
+ module Scorers
3
+ class CompositeScorer < BaseScorer
4
+ # Weights will grow as we add more scorers
5
+ WEIGHTS = {
6
+ query: 0.40,
7
+ n_plus_one: 0.60
8
+ }.freeze
9
+
10
+ def score
11
+ clamp(
12
+ (QueryScorer.new(@collector).score * WEIGHTS[:query]).round +
13
+ (NPlusOneScorer.new(@collector).score * WEIGHTS[:n_plus_one]).round
14
+ )
15
+ end
16
+
17
+ def label
18
+ case score
19
+ when BaseScorer::HEALTHY then "Healthy"
20
+ when BaseScorer::ACCEPTABLE then "Acceptable"
21
+ when BaseScorer::WARNING then "Warning"
22
+ else "Critical"
23
+ end
24
+ end
25
+
26
+ def color
27
+ case score
28
+ when BaseScorer::HEALTHY then "green"
29
+ when BaseScorer::ACCEPTABLE then "blue"
30
+ when BaseScorer::WARNING then "amber"
31
+ else "red"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ module RailsVitals
2
+ module Scorers
3
+ class NPlusOneScorer < BaseScorer
4
+ # Minimum times the same query must repeat to be flagged
5
+ REPEAT_THRESHOLD = 3
6
+
7
+ def score
8
+ return 100 if n_plus_one_count.zero?
9
+
10
+ # Each N+1 pattern costs 25 points, floored at 0
11
+ clamp(100 - (n_plus_one_count * 25))
12
+ end
13
+
14
+ def n_plus_one_patterns
15
+ query_fingerprints
16
+ .select { |_fingerprint, count| count >= REPEAT_THRESHOLD }
17
+ end
18
+
19
+ private
20
+
21
+ def n_plus_one_count
22
+ n_plus_one_patterns.size
23
+ end
24
+
25
+ def query_fingerprints
26
+ @collector.queries
27
+ .select { |q| q.is_a?(Hash) && q[:sql].is_a?(String) }
28
+ .map { |q| normalize(q[:sql]) }
29
+ .tally
30
+ end
31
+
32
+ # Strip values to compare query structure, not data
33
+ def normalize(sql)
34
+ sql
35
+ .gsub(/\d+/, "?")
36
+ .gsub(/'[^']*'/, "?")
37
+ .gsub(/\s+/, " ")
38
+ .strip
39
+ .downcase
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ module RailsVitals
2
+ module Scorers
3
+ class QueryScorer < BaseScorer
4
+ def score
5
+ clamp(count_score + time_score)
6
+ end
7
+
8
+ private
9
+
10
+ def count_score
11
+ count = @collector.total_query_count
12
+ warn_threshold = RailsVitals.config.query_warn_threshold
13
+ critical_threshold = RailsVitals.config.query_critical_threshold
14
+
15
+ if count <= warn_threshold
16
+ 50 # full points
17
+ elsif count <= critical_threshold
18
+ # linear decay between warn and critical
19
+ ratio = (count - warn_threshold).to_f / (critical_threshold - warn_threshold)
20
+ (50 * (1 - ratio)).round
21
+ else
22
+ 0
23
+ end
24
+ end
25
+
26
+ def time_score
27
+ time_ms = @collector.total_db_time_ms
28
+ warn_ms = RailsVitals.config.db_time_warn_ms
29
+ critical_ms = RailsVitals.config.db_time_critical_ms
30
+
31
+ if time_ms <= warn_ms
32
+ 50
33
+ elsif time_ms <= critical_ms
34
+ ratio = (time_ms - warn_ms).to_f / (critical_ms - warn_ms)
35
+ (50 * (1 - ratio)).round
36
+ else
37
+ 0
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ module RailsVitals
2
+ class Store
3
+ def initialize(size)
4
+ @size = size
5
+ @records = []
6
+ @mutex = Mutex.new
7
+ end
8
+
9
+ def push(record)
10
+ @mutex.synchronize do
11
+ @records.push(record)
12
+ @records.shift if @records.size > @size
13
+ end
14
+ end
15
+
16
+ def all
17
+ @mutex.synchronize { @records.dup }
18
+ end
19
+
20
+ def find(id)
21
+ @mutex.synchronize do
22
+ @records.find { |r| r.id == id }
23
+ end
24
+ end
25
+
26
+ def clear
27
+ @mutex.synchronize { @records.clear }
28
+ end
29
+
30
+ def size
31
+ @mutex.synchronize { @records.size }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module RailsVitals
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ require "rails_vitals/version"
2
+ require "rails_vitals/configuration"
3
+ require "rails_vitals/store"
4
+ require "rails_vitals/collector"
5
+ require "rails_vitals/request_record"
6
+ require "rails_vitals/notifications/subscriber"
7
+ require "rails_vitals/instrumentation/callback_instrumentation"
8
+ require "rails_vitals/analyzers/n_plus_one_aggregator"
9
+ require "rails_vitals/analyzers/sql_tokenizer"
10
+ require "rails_vitals/analyzers/association_mapper"
11
+ require "rails_vitals/scorers/base_scorer"
12
+ require "rails_vitals/scorers/query_scorer"
13
+ require "rails_vitals/scorers/n_plus_one_scorer"
14
+ require "rails_vitals/scorers/composite_scorer"
15
+ require "rails_vitals/panel_renderer"
16
+ require "rails_vitals/middleware/panel_injector"
17
+ require "rails_vitals/engine"
18
+
19
+ module RailsVitals
20
+ class << self
21
+ def configure
22
+ yield config
23
+ end
24
+
25
+ def config
26
+ @config ||= Configuration.new
27
+ end
28
+
29
+ def store
30
+ @store ||= Store.new(config.store_size)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :rails_vitals do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_vitals
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - David Sanchez
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.1'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 8.1.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '8.1'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 8.1.2
32
+ description: RailsVitals is a lightweight Rails engine gem that makes the hidden runtime
33
+ behavior of a Rails application visible, measurable, and teachable. It provides
34
+ insights into the inner workings of a Rails app, helping developers understand and
35
+ optimize their code. With RailsVitals, you can easily identify performance bottlenecks,
36
+ track database queries, and gain a deeper understanding of how your application
37
+ operates under the hood.
38
+ email:
39
+ - sanchez.dav90@gmail.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - MIT-LICENSE
45
+ - README.md
46
+ - Rakefile
47
+ - app/assets/stylesheets/rails_vitals/application.css
48
+ - app/controllers/rails_vitals/application_controller.rb
49
+ - app/controllers/rails_vitals/associations_controller.rb
50
+ - app/controllers/rails_vitals/dashboard_controller.rb
51
+ - app/controllers/rails_vitals/heatmap_controller.rb
52
+ - app/controllers/rails_vitals/models_controller.rb
53
+ - app/controllers/rails_vitals/n_plus_ones_controller.rb
54
+ - app/controllers/rails_vitals/requests_controller.rb
55
+ - app/helpers/rails_vitals/application_helper.rb
56
+ - app/jobs/rails_vitals/application_job.rb
57
+ - app/mailers/rails_vitals/application_mailer.rb
58
+ - app/models/rails_vitals/application_record.rb
59
+ - app/views/layouts/rails_vitals/application.html.erb
60
+ - app/views/rails_vitals/associations/index.html.erb
61
+ - app/views/rails_vitals/dashboard/index.html.erb
62
+ - app/views/rails_vitals/heatmap/index.html.erb
63
+ - app/views/rails_vitals/models/index.html.erb
64
+ - app/views/rails_vitals/n_plus_ones/index.html.erb
65
+ - app/views/rails_vitals/n_plus_ones/show.html.erb
66
+ - app/views/rails_vitals/requests/index.html.erb
67
+ - app/views/rails_vitals/requests/show.html.erb
68
+ - config/routes.rb
69
+ - lib/rails_vitals.rb
70
+ - lib/rails_vitals/analyzers/association_mapper.rb
71
+ - lib/rails_vitals/analyzers/n_plus_one_aggregator.rb
72
+ - lib/rails_vitals/analyzers/sql_tokenizer.rb
73
+ - lib/rails_vitals/collector.rb
74
+ - lib/rails_vitals/configuration.rb
75
+ - lib/rails_vitals/engine.rb
76
+ - lib/rails_vitals/instrumentation/callback_instrumentation.rb
77
+ - lib/rails_vitals/middleware/panel_injector.rb
78
+ - lib/rails_vitals/notifications/subscriber.rb
79
+ - lib/rails_vitals/panel_renderer.rb
80
+ - lib/rails_vitals/request_record.rb
81
+ - lib/rails_vitals/scorers/base_scorer.rb
82
+ - lib/rails_vitals/scorers/composite_scorer.rb
83
+ - lib/rails_vitals/scorers/n_plus_one_scorer.rb
84
+ - lib/rails_vitals/scorers/query_scorer.rb
85
+ - lib/rails_vitals/store.rb
86
+ - lib/rails_vitals/version.rb
87
+ - lib/tasks/rails_vitals_tasks.rake
88
+ homepage: https://github.com/Sanchezdav/rails_vitals
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://github.com/Sanchezdav/rails_vitals
93
+ source_code_uri: https://github.com/Sanchezdav/rails_vitals/tree/main
94
+ changelog_uri: https://github.com/Sanchezdav/rails_vitals/CHANGELOG.md
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.0.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.7.2
110
+ specification_version: 4
111
+ summary: RailsVitals is a lightweight Rails engine gem that makes the hidden runtime
112
+ behavior of a Rails application visible, measurable, and teachable.
113
+ test_files: []