rails-profiler 0.31.1 → 0.31.3

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -0
  3. data/app/assets/builds/profiler-toolbar.js +13 -6
  4. data/app/assets/builds/profiler.js +73 -37
  5. data/app/controllers/profiler/api/profiles_controller.rb +2 -0
  6. data/app/controllers/profiler/api/toolbar_controller.rb +2 -0
  7. data/app/controllers/profiler/profiles_controller.rb +2 -0
  8. data/lib/profiler/allocation_counter.rb +21 -0
  9. data/lib/profiler/collectors/base_collector.rb +39 -3
  10. data/lib/profiler/collectors/cache_collector.rb +8 -3
  11. data/lib/profiler/collectors/database_collector.rb +133 -10
  12. data/lib/profiler/collectors/env_collector.rb +12 -4
  13. data/lib/profiler/collectors/exception_collector.rb +12 -9
  14. data/lib/profiler/collectors/flamegraph_collector.rb +10 -5
  15. data/lib/profiler/collectors/function_profiler_collector.rb +28 -4
  16. data/lib/profiler/collectors/http_collector.rb +5 -0
  17. data/lib/profiler/collectors/log_collector.rb +5 -0
  18. data/lib/profiler/collectors/mailer_collector.rb +2 -8
  19. data/lib/profiler/collectors/request_collector.rb +14 -11
  20. data/lib/profiler/collectors/routes_collector.rb +7 -50
  21. data/lib/profiler/collectors/scoped_notifications.rb +229 -0
  22. data/lib/profiler/collectors/view_collector.rb +7 -2
  23. data/lib/profiler/configuration.rb +27 -4
  24. data/lib/profiler/console_profiler.rb +2 -17
  25. data/lib/profiler/instrumentation/executor_context_propagation.rb +47 -0
  26. data/lib/profiler/instrumentation/request_context.rb +77 -0
  27. data/lib/profiler/instrumentation/thread_context_propagation.rb +11 -16
  28. data/lib/profiler/job_profiler.rb +6 -17
  29. data/lib/profiler/mcp/resources/recent_requests.rb +2 -0
  30. data/lib/profiler/mcp/tools/get_profile_detail.rb +21 -2
  31. data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
  32. data/lib/profiler/middleware/capturing_body.rb +112 -0
  33. data/lib/profiler/middleware/profiler_middleware.rb +261 -53
  34. data/lib/profiler/middleware/streamed_profile.rb +135 -0
  35. data/lib/profiler/middleware/toolbar_injector.rb +3 -10
  36. data/lib/profiler/models/profile.rb +69 -5
  37. data/lib/profiler/process_snapshot.rb +157 -0
  38. data/lib/profiler/redaction.rb +10 -0
  39. data/lib/profiler/test_profiler.rb +2 -17
  40. data/lib/profiler/version.rb +1 -1
  41. data/lib/profiler.rb +6 -0
  42. metadata +12 -5
@@ -129,17 +129,10 @@ module Profiler
129
129
  end
130
130
  end
131
131
 
132
+ # The middleware hands over the page it has already read: an Array of parts, or a String.
133
+ # Never a body to iterate, which would drain a stream and close it.
132
134
  def extract_content(body)
133
- if body.respond_to?(:body)
134
- body.body
135
- elsif body.respond_to?(:each)
136
- parts = []
137
- body.each { |part| parts << part }
138
- body.close if body.respond_to?(:close)
139
- parts.join
140
- else
141
- body.to_s
142
- end
135
+ body.is_a?(String) ? body : Array(body).join
143
136
  end
144
137
 
145
138
  def toolbar_html
@@ -5,17 +5,21 @@ require "securerandom"
5
5
  require "json"
6
6
  require "zlib"
7
7
  require_relative "../redaction"
8
+ require_relative "../allocation_counter"
8
9
 
9
10
  module Profiler
10
11
  module Models
11
12
  class Profile
12
13
  attr_reader :path
13
- attr_accessor :token, :method, :status, :duration, :memory,
14
+ attr_accessor :token, :method, :status, :duration, :allocated_objects,
14
15
  :started_at, :finished_at, :params, :headers,
15
16
  :response_headers, :collectors_data, :collectors_metadata,
16
17
  :parent_token, :is_ajax, :profile_type,
17
18
  :request_body, :request_body_encoding,
18
19
  :response_body, :response_body_encoding,
20
+ :request_body_size, :request_body_truncated, :request_body_size_is_minimum,
21
+ :response_body_size, :response_body_truncated, :response_body_size_is_minimum,
22
+ :collectors_released_after_seconds,
19
23
  :gem_version
20
24
 
21
25
  def initialize(request = nil)
@@ -35,13 +39,32 @@ module Profiler
35
39
  end
36
40
  end
37
41
 
38
- def set_bodies(request_body:, response_body:, req_content_type:, resp_content_type:)
42
+ # The sizes are those of the whole bodies, when a body was cut at max_captured_body_bytes;
43
+ # *_size_is_minimum when the whole size is not known (no Content-Length, a stream that
44
+ # stopped), only that it is at least that.
45
+ def set_bodies(request_body:, response_body:, req_content_type:, resp_content_type:,
46
+ request_body_size: nil, response_body_size: nil,
47
+ request_body_size_is_minimum: false, response_body_size_is_minimum: false)
39
48
  req = process_body(request_body, req_content_type)
40
49
  resp = process_body(response_body, resp_content_type)
41
50
  @request_body = req[:body]
42
51
  @request_body_encoding = req[:encoding]
43
52
  @response_body = resp[:body]
44
53
  @response_body_encoding = resp[:encoding]
54
+ @request_body_size, @request_body_truncated = body_size(request_body, request_body_size)
55
+ @response_body_size, @response_body_truncated = body_size(response_body, response_body_size)
56
+ @request_body_size_is_minimum = request_body_size_is_minimum ? true : false
57
+ @response_body_size_is_minimum = response_body_size_is_minimum ? true : false
58
+ end
59
+
60
+ # Deprecated: the number of allocated objects times 40, the figure earlier versions
61
+ # reported as bytes. Still written in to_h, for the readers that only know it.
62
+ def memory
63
+ @allocated_objects&.*(AllocationCounter::LEGACY_BYTES_PER_OBJECT)
64
+ end
65
+
66
+ def memory=(value)
67
+ @allocated_objects = value && value / AllocationCounter::LEGACY_BYTES_PER_OBJECT
45
68
  end
46
69
 
47
70
  def finish(status, response_headers = {})
@@ -67,8 +90,20 @@ module Profiler
67
90
  end
68
91
 
69
92
  def add_collector_metadata(collector)
93
+ @collectors_metadata << collector_metadata(collector)
94
+ end
95
+
96
+ # A collector collected again, after the profile's tabs were listed (a streamed response
97
+ # that failed part way): its tab keeps its place, with what it now has.
98
+ def refresh_collector_metadata(collector)
99
+ entry = collector_metadata(collector)
100
+ index = @collectors_metadata.index { |tab| tab[:key] == entry[:key] }
101
+ index ? @collectors_metadata[index] = entry : @collectors_metadata << entry
102
+ end
103
+
104
+ def collector_metadata(collector)
70
105
  config = collector.tab_config
71
- @collectors_metadata << {
106
+ {
72
107
  key: config[:key],
73
108
  label: config[:label],
74
109
  icon: config[:icon],
@@ -79,6 +114,7 @@ module Profiler
79
114
  has_data: collector.has_data?
80
115
  }
81
116
  end
117
+ private :collector_metadata
82
118
 
83
119
  def to_h
84
120
  req_body, req_enc = decode_body(@request_body, @request_body_encoding)
@@ -92,7 +128,8 @@ module Profiler
92
128
  method: @method,
93
129
  status: @status,
94
130
  duration: @duration,
95
- memory: @memory,
131
+ allocated_objects: @allocated_objects,
132
+ memory: memory,
96
133
  started_at: @started_at&.iso8601,
97
134
  finished_at: @finished_at&.iso8601,
98
135
  params: @params,
@@ -102,6 +139,13 @@ module Profiler
102
139
  request_body_encoding: req_enc,
103
140
  response_body: resp_body,
104
141
  response_body_encoding: resp_enc,
142
+ request_body_size: @request_body_size,
143
+ request_body_truncated: @request_body_truncated,
144
+ request_body_size_is_minimum: @request_body_size_is_minimum,
145
+ response_body_size: @response_body_size,
146
+ response_body_truncated: @response_body_truncated,
147
+ response_body_size_is_minimum: @response_body_size_is_minimum,
148
+ collectors_released_after_seconds: @collectors_released_after_seconds,
105
149
  collectors_data: @collectors_data,
106
150
  tabs: @collectors_metadata,
107
151
  parent_token: @parent_token,
@@ -125,7 +169,12 @@ module Profiler
125
169
  profile.method = data[:method]
126
170
  profile.status = data[:status]
127
171
  profile.duration = data[:duration]
128
- profile.memory = data[:memory]
172
+ # A profile saved before allocated_objects only carries memory, the count times 40.
173
+ if data.key?(:allocated_objects)
174
+ profile.allocated_objects = data[:allocated_objects]
175
+ else
176
+ profile.memory = data[:memory]
177
+ end
129
178
  profile.started_at = data[:started_at] ? Time.parse(data[:started_at]) : nil
130
179
  profile.finished_at = data[:finished_at] ? Time.parse(data[:finished_at]) : nil
131
180
  profile.params = data[:params]
@@ -135,6 +184,15 @@ module Profiler
135
184
  profile.request_body_encoding = data[:request_body_encoding] || "text"
136
185
  profile.response_body = data[:response_body]
137
186
  profile.response_body_encoding = data[:response_body_encoding] || "text"
187
+ profile.request_body_size = data[:request_body_size]
188
+ # Left nil when the store did not keep them (SqliteStore rebuilds a profile from its
189
+ # columns): the request collector's data still has them.
190
+ profile.request_body_truncated = data[:request_body_truncated]
191
+ profile.request_body_size_is_minimum = data[:request_body_size_is_minimum]
192
+ profile.response_body_size = data[:response_body_size]
193
+ profile.response_body_truncated = data[:response_body_truncated]
194
+ profile.response_body_size_is_minimum = data[:response_body_size_is_minimum]
195
+ profile.collectors_released_after_seconds = data[:collectors_released_after_seconds]
138
196
  profile.parent_token = data[:parent_token]
139
197
  profile.is_ajax = data[:is_ajax] || false
140
198
  profile.profile_type = data[:profile_type] || "http"
@@ -181,6 +239,12 @@ module Profiler
181
239
  end
182
240
  end
183
241
 
242
+ def body_size(captured, total)
243
+ captured_size = captured.to_s.bytesize
244
+ total ||= captured_size
245
+ [total, total > captured_size]
246
+ end
247
+
184
248
  def compress_body?(text)
185
249
  Profiler.configuration.compress_bodies &&
186
250
  text.bytesize > Profiler.configuration.compress_body_threshold
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redaction"
4
+
5
+ module Profiler
6
+ # What is the same for every request of a process, kept out of the profiles: the route table
7
+ # and ENV. A profile stores the route its request matched; the Routes and Env tabs, the
8
+ # toolbar and the MCP tools get the table and the variables from the process that displays the
9
+ # profile (#hydrate).
10
+ #
11
+ # The route table is built once and built again when the routes are reloaded, in development
12
+ # (reload_routes!, or a change picked up by the reloader): the routes are new objects then.
13
+ # ENV is read when the profile is displayed, masked as it always was: it can change while the
14
+ # process runs, through the profiler's own env overrides among others.
15
+ module ProcessSnapshot
16
+ @mutex = Mutex.new
17
+ @routes = nil # [fingerprint, table, routes by [controller, action]]
18
+
19
+ class << self
20
+ # The application's routes, the engine's and Rails' own left out, as the Routes tab lists
21
+ # them: { name:, pattern:, verb:, controller_action: }.
22
+ def route_table
23
+ routes_snapshot[1]
24
+ end
25
+
26
+ # The route of the table a request to +path+ with +method+ is dispatched to, or nil.
27
+ def matched_route(path, method)
28
+ return nil unless rails_routes?
29
+
30
+ recognized = Rails.application.routes.recognize_path(path, method: method)
31
+ candidates = routes_snapshot[2][[recognized[:controller], recognized[:action]]]
32
+ return nil unless candidates
33
+
34
+ # A PUT goes to the action of the PATCH route too: give the route of the request's verb.
35
+ verb = method.to_s.upcase == "HEAD" ? "GET" : method.to_s.upcase
36
+ candidates.find { |route| route[:verb].split("|").include?(verb) } ||
37
+ candidates.find { |route| route[:verb] == "ANY" } ||
38
+ candidates.first
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ # ENV as the Env tab shows it.
44
+ def env_variables
45
+ Redaction.hide_credentials(Redaction.env_snapshot)
46
+ end
47
+
48
+ # Puts the route table and ENV back into +profile+'s data for display. A profile saved by
49
+ # an earlier version has its own table and variables, and is left as it is.
50
+ def hydrate(profile)
51
+ # A job, a console expression or a test ran in another process, whose ENV its profile keeps.
52
+ return profile unless profile && profile.profile_type.to_s == "http"
53
+
54
+ data = profile.collectors_data
55
+ if (routes = data["routes"]) && !key?(routes, :routes)
56
+ data["routes"] = with_route_table(routes)
57
+ end
58
+ if (env = data["env"]) && !key?(env, :variables)
59
+ variables = env_variables
60
+ data["env"] = merge(env, variables: variables, total: variables.size)
61
+ end
62
+ profile
63
+ rescue StandardError => e
64
+ warn "Profiler: could not add the routes and ENV to profile #{profile.token}: #{e.message}"
65
+ profile
66
+ end
67
+
68
+ private
69
+
70
+ def rails_routes?
71
+ defined?(Rails) && Rails.respond_to?(:application) && Rails.application
72
+ end
73
+
74
+ def routes_snapshot
75
+ return [nil, [].freeze, {}.freeze] unless rails_routes?
76
+
77
+ journey = Rails.application.routes.routes
78
+ list = journey.respond_to?(:routes) ? journey.routes : journey.to_a
79
+ fingerprint = [journey.object_id, list.size, list.first&.object_id, list.last&.object_id]
80
+ snapshot = @routes
81
+ return snapshot if snapshot && snapshot[0] == fingerprint
82
+
83
+ @mutex.synchronize do
84
+ @routes = build_snapshot(fingerprint, list) unless @routes && @routes[0] == fingerprint
85
+ @routes
86
+ end
87
+ rescue StandardError
88
+ [nil, [].freeze, {}.freeze]
89
+ end
90
+
91
+ def build_snapshot(fingerprint, list)
92
+ index = {}
93
+ table = list.filter_map do |route|
94
+ next if route.respond_to?(:internal) && route.internal
95
+
96
+ controller = route.defaults[:controller]
97
+ action = route.defaults[:action]
98
+ next if controller.nil?
99
+ next if controller.start_with?("rails/", "profiler/")
100
+
101
+ verb = route.verb
102
+ entry = {
103
+ name: route.name,
104
+ pattern: route.path.spec.to_s.sub(/\(\.:format\)$/, ""),
105
+ verb: (verb && !verb.empty?) ? verb : "ANY",
106
+ controller_action: controller_action(controller, action)
107
+ }.freeze
108
+ (index[[controller, action]] ||= []) << entry
109
+ entry
110
+ end
111
+ [fingerprint, table.freeze, index.each_value(&:freeze).freeze]
112
+ end
113
+
114
+ def controller_action(controller, action)
115
+ return nil unless action
116
+
117
+ "#{controller.split("/").map { |s| ActiveSupport::Inflector.camelize(s) }.join("::")}Controller##{action}"
118
+ end
119
+
120
+ def with_route_table(routes)
121
+ matched = fetch(routes, :matched)
122
+ table = route_table.map { |route| route.merge(matched: same_route?(route, matched)) }
123
+ merge(routes, routes: table, total: table.size)
124
+ end
125
+
126
+ # Every route of the matched action, as the table always marked them.
127
+ def same_route?(route, matched)
128
+ return false unless matched.is_a?(Hash)
129
+
130
+ action = fetch(matched, :controller_action)
131
+ action ? route[:controller_action] == action : route[:pattern] == fetch(matched, :pattern)
132
+ end
133
+
134
+ # Collector data has symbol keys as collected, string keys once read back from storage.
135
+ def key?(hash, key)
136
+ hash.key?(key) || hash.key?(key.to_s)
137
+ end
138
+
139
+ def fetch(hash, key)
140
+ hash.key?(key) ? hash[key] : hash[key.to_s]
141
+ end
142
+
143
+ def merge(hash, values)
144
+ strings = hash.keys.first.is_a?(String)
145
+ hash.merge(strings ? deep_stringify(values) : values)
146
+ end
147
+
148
+ def deep_stringify(value)
149
+ case value
150
+ when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
151
+ when Array then value.map { |v| deep_stringify(v) }
152
+ else value
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -88,6 +88,16 @@ module Profiler
88
88
  text.length > max ? "#{text[0, max]}#{ellipsis}" : text
89
89
  end
90
90
 
91
+ # The first +max+ bytes of +text+, a body kept only in part. A credential cut in two by the
92
+ # limit would escape the masking by value, which runs later on what is kept: the bytes
93
+ # that could start one are left out as well. Text no longer than +max+ is returned whole.
94
+ def cut_bytes(text, max)
95
+ return text if text.bytesize <= max
96
+
97
+ cut = text.byteslice(0, max)
98
+ cut.byteslice(0, cut.bytesize - held_back_bytes(cut))
99
+ end
100
+
91
101
  # How many trailing bytes of +text+, a stream read in pieces, could be
92
102
  # the start of a credential and have to wait for the next piece: the
93
103
  # longest end of the text that is also the beginning of a credential,
@@ -56,7 +56,7 @@ module Profiler
56
56
 
57
57
  exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
58
58
 
59
- memory_before = current_memory if Profiler.configuration.track_memory
59
+ allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
60
60
 
61
61
  test_status = "passed"
62
62
  error_message = nil
@@ -103,7 +103,7 @@ module Profiler
103
103
  Profiler::CurrentContext.token = previous_token
104
104
 
105
105
  if Profiler.configuration.track_memory
106
- profile.memory = current_memory - memory_before
106
+ profile.allocated_objects = AllocationCounter.current - allocations_before
107
107
  end
108
108
 
109
109
  test_collector.update_status(test_status, error_message)
@@ -124,20 +124,5 @@ module Profiler
124
124
  # After collect, and also when collect or the storage failed.
125
125
  Collectors::Lifecycle.release_all(collectors)
126
126
  end
127
-
128
- private
129
-
130
- def current_memory
131
- return 0 unless defined?(GC.stat)
132
-
133
- stats = GC.stat
134
- if stats.key?(:total_allocated_size)
135
- stats[:total_allocated_size]
136
- elsif stats.key?(:total_allocated_objects)
137
- stats[:total_allocated_objects] * 40
138
- else
139
- 0
140
- end
141
- end
142
127
  end
143
128
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.31.1"
4
+ VERSION = "0.31.3"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "profiler/boot_env"
5
5
  require_relative "profiler/version"
6
6
  require_relative "profiler/configuration"
7
7
  require_relative "profiler/redaction"
8
+ require_relative "profiler/allocation_counter"
8
9
 
9
10
  module Profiler
10
11
  class Error < StandardError; end
@@ -15,6 +16,9 @@ module Profiler
15
16
  attr_accessor :function_profiling_max_frames
16
17
  attr_accessor :function_profiling_mode
17
18
  attr_accessor :function_profiling_clock
19
+ # Without stackprof, the function profiler stays off unless this is true: it then traces
20
+ # every method call of the request's thread with a TracePoint, several times slower.
21
+ attr_accessor :function_profiling_tracepoint_fallback
18
22
 
19
23
  def configuration
20
24
  @configuration ||= Configuration.new
@@ -113,6 +117,7 @@ module Profiler
113
117
  self.function_profiling_max_frames = 2000
114
118
  self.function_profiling_mode = "lite"
115
119
  self.function_profiling_clock = "wall"
120
+ self.function_profiling_tracepoint_fallback = false
116
121
  end
117
122
 
118
123
  # Require core components
@@ -137,6 +142,7 @@ require_relative "profiler/storage/token"
137
142
  require_relative "profiler/storage/private_files"
138
143
  require_relative "profiler/env_override_store"
139
144
  require_relative "profiler/instrumentation/thread_context_propagation"
145
+ require_relative "profiler/instrumentation/executor_context_propagation"
140
146
  require_relative "profiler/sse/event_bus"
141
147
  require_relative "profiler/sse/redis_event_bus"
142
148
  require_relative "profiler/sse/bus"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.1
4
+ version: 0.31.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
@@ -133,6 +133,7 @@ files:
133
133
  - config/routes.rb
134
134
  - exe/profiler-mcp
135
135
  - lib/profiler.rb
136
+ - lib/profiler/allocation_counter.rb
136
137
  - lib/profiler/boot_env.rb
137
138
  - lib/profiler/cluster/master_client.rb
138
139
  - lib/profiler/cluster/security.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/profiler/collectors/mailer_collector.rb
158
159
  - lib/profiler/collectors/request_collector.rb
159
160
  - lib/profiler/collectors/routes_collector.rb
161
+ - lib/profiler/collectors/scoped_notifications.rb
160
162
  - lib/profiler/collectors/test_collector.rb
161
163
  - lib/profiler/collectors/view_collector.rb
162
164
  - lib/profiler/configuration.rb
@@ -166,8 +168,10 @@ files:
166
168
  - lib/profiler/env_override_store.rb
167
169
  - lib/profiler/explain_runner.rb
168
170
  - lib/profiler/instrumentation/active_job_instrumentation.rb
171
+ - lib/profiler/instrumentation/executor_context_propagation.rb
169
172
  - lib/profiler/instrumentation/irb_instrumentation.rb
170
173
  - lib/profiler/instrumentation/net_http_instrumentation.rb
174
+ - lib/profiler/instrumentation/request_context.rb
171
175
  - lib/profiler/instrumentation/sidekiq_middleware.rb
172
176
  - lib/profiler/instrumentation/thread_context_propagation.rb
173
177
  - lib/profiler/job_profiler.rb
@@ -206,12 +210,15 @@ files:
206
210
  - lib/profiler/mcp/tools/reset_env_var.rb
207
211
  - lib/profiler/mcp/tools/run_tests.rb
208
212
  - lib/profiler/mcp/tools/set_env_var.rb
213
+ - lib/profiler/middleware/capturing_body.rb
209
214
  - lib/profiler/middleware/cors_middleware.rb
210
215
  - lib/profiler/middleware/profiler_middleware.rb
216
+ - lib/profiler/middleware/streamed_profile.rb
211
217
  - lib/profiler/middleware/toolbar_injector.rb
212
218
  - lib/profiler/models/profile.rb
213
219
  - lib/profiler/models/sql_query.rb
214
220
  - lib/profiler/models/timeline_event.rb
221
+ - lib/profiler/process_snapshot.rb
215
222
  - lib/profiler/railtie.rb
216
223
  - lib/profiler/redaction.rb
217
224
  - lib/profiler/sql_statement.rb
@@ -235,13 +242,13 @@ files:
235
242
  - lib/profiler/test_runner/run_store.rb
236
243
  - lib/profiler/test_runner/runner.rb
237
244
  - lib/profiler/version.rb
238
- homepage: https://git.duplessy.eu/sebastien/rails-profiler-gem
245
+ homepage: https://github.com/Pepsit36/rails-profiler
239
246
  licenses:
240
247
  - MIT
241
248
  metadata:
242
- homepage_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem
243
- source_code_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem
244
- changelog_uri: https://git.duplessy.eu/sebastien/rails-profiler-gem/-/blob/master/CHANGELOG.md
249
+ homepage_uri: https://github.com/Pepsit36/rails-profiler
250
+ source_code_uri: https://github.com/Pepsit36/rails-profiler
251
+ changelog_uri: https://github.com/Pepsit36/rails-profiler/blob/master/CHANGELOG.md
245
252
  post_install_message:
246
253
  rdoc_options: []
247
254
  require_paths: