rails-profiler 0.31.3 → 0.31.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +129 -0
- data/app/assets/builds/profiler.js +9 -1
- data/app/controllers/profiler/api/console_controller.rb +1 -11
- data/app/controllers/profiler/api/jobs_controller.rb +1 -11
- data/app/controllers/profiler/api/profiles_controller.rb +18 -28
- data/app/controllers/profiler/api/tests_controller.rb +1 -11
- data/app/controllers/profiler/api/toolbar_controller.rb +0 -20
- data/app/controllers/profiler/application_controller.rb +55 -15
- data/app/controllers/profiler/profiles_controller.rb +2 -18
- data/lib/profiler/ajax_data.rb +40 -0
- data/lib/profiler/cluster/master_client.rb +7 -15
- data/lib/profiler/cluster/security.rb +3 -3
- data/lib/profiler/cluster/slave_proxy.rb +14 -9
- data/lib/profiler/collectors/lifecycle.rb +2 -2
- data/lib/profiler/collectors/log_collector.rb +54 -5
- data/lib/profiler/collectors/mailer_collector.rb +8 -4
- data/lib/profiler/collectors/request_collector.rb +6 -1
- data/lib/profiler/collectors/scoped_notifications.rb +1 -1
- data/lib/profiler/configuration.rb +30 -8
- data/lib/profiler/console_profiler.rb +1 -1
- data/lib/profiler/engine.rb +2 -1
- data/lib/profiler/env_override_store.rb +4 -4
- data/lib/profiler/explain_runner.rb +5 -1
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +138 -40
- data/lib/profiler/job_profiler.rb +1 -1
- data/lib/profiler/local_request.rb +2 -6
- data/lib/profiler/mcp/file_cache.rb +2 -1
- data/lib/profiler/mcp/resources/failing_tests.rb +1 -1
- data/lib/profiler/mcp/resources/recent_console.rb +2 -2
- data/lib/profiler/mcp/resources/recent_jobs.rb +2 -2
- data/lib/profiler/mcp/resources/slow_tests.rb +1 -1
- data/lib/profiler/mcp/server.rb +9 -3
- data/lib/profiler/mcp/tools/get_profile_ajax.rb +2 -0
- data/lib/profiler/mcp/tools/get_profile_http.rb +6 -1
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +1 -1
- data/lib/profiler/mcp/tools/query_console_profiles.rb +6 -2
- data/lib/profiler/mcp/tools/query_jobs.rb +6 -2
- data/lib/profiler/mcp/tools/query_mailers.rb +5 -1
- data/lib/profiler/mcp/tools/query_profiles.rb +6 -2
- data/lib/profiler/mcp/tools/query_test_profiles.rb +6 -2
- data/lib/profiler/mcp/tools/run_tests.rb +5 -2
- data/lib/profiler/middleware/profiler_middleware.rb +22 -16
- data/lib/profiler/middleware/streamed_profile.rb +1 -1
- data/lib/profiler/middleware/toolbar_injector.rb +21 -2
- data/lib/profiler/models/profile.rb +60 -2
- data/lib/profiler/process_snapshot.rb +8 -3
- data/lib/profiler/railtie.rb +4 -1
- data/lib/profiler/redaction.rb +1 -3
- data/lib/profiler/sse/redis_event_bus.rb +2 -1
- data/lib/profiler/storage/base_store.rb +4 -2
- data/lib/profiler/storage/blob_store.rb +1 -1
- data/lib/profiler/storage/file_store.rb +337 -58
- data/lib/profiler/storage/memory_store.rb +60 -31
- data/lib/profiler/storage/private_files.rb +16 -3
- data/lib/profiler/storage/redis_store.rb +252 -33
- data/lib/profiler/storage/sqlite_store.rb +59 -11
- data/lib/profiler/storage/summary.rb +50 -0
- data/lib/profiler/storage/unavailable.rb +57 -0
- data/lib/profiler/test_helpers/reporter.rb +2 -2
- data/lib/profiler/test_profiler.rb +1 -1
- data/lib/profiler/test_runner/runner.rb +3 -7
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +138 -4
- metadata +4 -1
|
@@ -5,7 +5,7 @@ module Profiler
|
|
|
5
5
|
module Resources
|
|
6
6
|
class SlowTests
|
|
7
7
|
def self.call
|
|
8
|
-
profiles = Profiler.storage.list(limit: 500)
|
|
8
|
+
profiles = Profiler.storage.list(limit: 500, type: "test")
|
|
9
9
|
tests = profiles.select { |p| p.profile_type == "test" }
|
|
10
10
|
.sort_by { |p| -p.duration }
|
|
11
11
|
.first(10)
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -35,8 +35,12 @@ module Profiler
|
|
|
35
35
|
handler = @resource_handlers[uri]
|
|
36
36
|
next [{ uri: uri, mimeType: "application/json", text: "Resource not found: #{uri}" }] unless handler
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
begin
|
|
39
|
+
result = handler.call
|
|
40
|
+
[{ uri: result[:uri], mimeType: result[:mimeType], text: result[:text] }]
|
|
41
|
+
rescue Profiler::Storage::Unavailable::Error => e
|
|
42
|
+
[{ uri: uri, mimeType: "text/plain", text: e.message }]
|
|
43
|
+
end
|
|
40
44
|
end
|
|
41
45
|
end
|
|
42
46
|
|
|
@@ -53,7 +57,7 @@ module Profiler
|
|
|
53
57
|
when :stdio
|
|
54
58
|
::MCP::Server::Transports::StdioTransport.new(@server).open
|
|
55
59
|
when :http
|
|
56
|
-
|
|
60
|
+
Profiler.log_info("MCP: HTTP transport active, endpoint /_profiler/mcp")
|
|
57
61
|
else
|
|
58
62
|
raise Error, "Unknown transport: #{transport}"
|
|
59
63
|
end
|
|
@@ -393,6 +397,8 @@ module Profiler
|
|
|
393
397
|
::MCP::Tool.define(name: name, description: description, input_schema: input_schema) do |server_context: nil, **args|
|
|
394
398
|
result = handler.call(args.transform_keys(&:to_s))
|
|
395
399
|
result.is_a?(::MCP::Tool::Response) ? result : ::MCP::Tool::Response.new(result)
|
|
400
|
+
rescue Profiler::Storage::Unavailable::Error => e
|
|
401
|
+
::MCP::Tool::Response.new([{ type: "text", text: e.message }], error: true)
|
|
396
402
|
end
|
|
397
403
|
end
|
|
398
404
|
|
|
@@ -22,6 +22,8 @@ module Profiler
|
|
|
22
22
|
return [{ type: "text", text: "Profile not found: #{token}" }]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The sub-requests are saved after the page: computed from its children, as the page does.
|
|
26
|
+
Profiler::AjaxData.attach(profile, storage.find_by_parent(profile.token))
|
|
25
27
|
ajax_data = profile.collector_data("ajax")
|
|
26
28
|
unless ajax_data && ajax_data["total_requests"].to_i > 0
|
|
27
29
|
return [{ type: "text", text: "No AJAX requests found in this profile" }]
|
|
@@ -85,8 +85,13 @@ module Profiler
|
|
|
85
85
|
lines << "- **URL:** #{req['url']}"
|
|
86
86
|
lines << "- **Status:** #{req['status'] == 0 ? 'connection error' : req['status']}"
|
|
87
87
|
lines << "- **Duration:** #{req['duration'].round(2)} ms"
|
|
88
|
-
lines << "- **Request Size:** #{req['request_size']} bytes"
|
|
88
|
+
lines << "- **Request Size:** #{req['request_size'].nil? ? 'unknown' : "#{'at least ' if req['request_size_is_minimum']}#{req['request_size']} bytes"}"
|
|
89
89
|
lines << "- **Response Size:** #{req['response_size']} bytes"
|
|
90
|
+
if req["request_body_not_captured"]
|
|
91
|
+
lines << "- _Request body not captured: a stream that cannot be rewound is sent unread._"
|
|
92
|
+
end
|
|
93
|
+
lines << "- _Request body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["request_body_truncated"]
|
|
94
|
+
lines << "- _Response body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["response_body_truncated"]
|
|
90
95
|
lines << "- **Error:** #{req['error']}" if req["error"]
|
|
91
96
|
|
|
92
97
|
if req["request_headers"] && !req["request_headers"].empty?
|
|
@@ -14,7 +14,7 @@ module Profiler
|
|
|
14
14
|
|
|
15
15
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
16
16
|
profile = if token == "latest"
|
|
17
|
-
profiles = storage.list(limit:
|
|
17
|
+
profiles = storage.list(limit: 1, type: "test")
|
|
18
18
|
profiles.find { |p| p.profile_type == "test" }
|
|
19
19
|
else
|
|
20
20
|
storage.load(token)
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "console")
|
|
16
16
|
|
|
17
17
|
consoles = profiles.select { |p| p.profile_type == "console" }
|
|
18
18
|
|
|
@@ -40,7 +40,11 @@ module Profiler
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
if params["cursor"]
|
|
43
|
-
cutoff =
|
|
43
|
+
cutoff = begin
|
|
44
|
+
Time.parse(params["cursor"])
|
|
45
|
+
rescue ArgumentError, TypeError
|
|
46
|
+
nil # a cursor that is not a time is ignored, as before
|
|
47
|
+
end
|
|
44
48
|
consoles = consoles.select { |p| p.started_at < cutoff } if cutoff
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "job")
|
|
16
16
|
|
|
17
17
|
jobs = profiles.select { |p| p.profile_type == "job" }
|
|
18
18
|
|
|
@@ -31,7 +31,11 @@ module Profiler
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
if params["cursor"]
|
|
34
|
-
cutoff =
|
|
34
|
+
cutoff = begin
|
|
35
|
+
Time.parse(params["cursor"])
|
|
36
|
+
rescue ArgumentError, TypeError
|
|
37
|
+
nil # a cursor that is not a time is ignored, as before
|
|
38
|
+
end
|
|
35
39
|
jobs = jobs.select { |p| p.started_at < cutoff } if cutoff
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -49,7 +49,11 @@ module Profiler
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
if params["cursor"]
|
|
52
|
-
cutoff =
|
|
52
|
+
cutoff = begin
|
|
53
|
+
Time.parse(params["cursor"])
|
|
54
|
+
rescue ArgumentError, TypeError
|
|
55
|
+
nil # a cursor that is not a time is ignored, as before
|
|
56
|
+
end
|
|
53
57
|
if cutoff
|
|
54
58
|
emails = emails.select do |e|
|
|
55
59
|
started = e["profile_started_at"]
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
13
13
|
limit = params["limit"]&.to_i || 20
|
|
14
14
|
fetch_size = [limit * 5, 500].min
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: params["profile_type"])
|
|
16
16
|
|
|
17
17
|
profiles = profiles.select { |p| p.path&.include?(params["path"]) } if params["path"]
|
|
18
18
|
profiles = profiles.select { |p| p.method == params["method"]&.upcase } if params["method"]
|
|
@@ -23,7 +23,11 @@ module Profiler
|
|
|
23
23
|
profiles = profiles.select { |p| p.profile_type == params["profile_type"] } if params["profile_type"]
|
|
24
24
|
|
|
25
25
|
if params["cursor"]
|
|
26
|
-
cutoff =
|
|
26
|
+
cutoff = begin
|
|
27
|
+
Time.parse(params["cursor"])
|
|
28
|
+
rescue ArgumentError, TypeError
|
|
29
|
+
nil # a cursor that is not a time is ignored, as before
|
|
30
|
+
end
|
|
27
31
|
profiles = profiles.select { |p| p.started_at < cutoff } if cutoff
|
|
28
32
|
end
|
|
29
33
|
|
|
@@ -12,7 +12,7 @@ module Profiler
|
|
|
12
12
|
limit = params["limit"]&.to_i || 20
|
|
13
13
|
fetch_size = [limit * 5, 500].min
|
|
14
14
|
storage = MCP::SlaveSupport.resolve_storage(params)
|
|
15
|
-
profiles = storage.list(limit: fetch_size)
|
|
15
|
+
profiles = storage.list(limit: fetch_size, type: "test")
|
|
16
16
|
|
|
17
17
|
tests = profiles.select { |p| p.profile_type == "test" }
|
|
18
18
|
|
|
@@ -37,7 +37,11 @@ module Profiler
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
if params["cursor"]
|
|
40
|
-
cutoff =
|
|
40
|
+
cutoff = begin
|
|
41
|
+
Time.parse(params["cursor"])
|
|
42
|
+
rescue ArgumentError, TypeError
|
|
43
|
+
nil # a cursor that is not a time is ignored, as before
|
|
44
|
+
end
|
|
41
45
|
tests = tests.select { |p| p.started_at < cutoff } if cutoff
|
|
42
46
|
end
|
|
43
47
|
|
|
@@ -114,10 +114,13 @@ module Profiler
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def self.collect_run_profiles(since)
|
|
117
|
-
Profiler.storage.list(limit: 500).select do |p|
|
|
117
|
+
Profiler.storage.list(limit: 500, type: "test").select do |p|
|
|
118
118
|
p.profile_type == "test" && p.started_at && p.started_at >= since
|
|
119
119
|
end.map(&:token)
|
|
120
|
-
rescue
|
|
120
|
+
rescue Profiler::Storage::Unavailable::Error
|
|
121
|
+
raise # answered by the MCP server as an error
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
Profiler.log_error("MCP run_tests: could not list the profiles of the run", e)
|
|
121
124
|
[]
|
|
122
125
|
end
|
|
123
126
|
|
|
@@ -42,7 +42,7 @@ module Profiler
|
|
|
42
42
|
env["profiler.collectors"] = collectors
|
|
43
43
|
subscribed = Collectors::Lifecycle.subscribe_all(collectors, "ProfilerMiddleware")
|
|
44
44
|
rescue => e
|
|
45
|
-
|
|
45
|
+
Profiler.log_error("ProfilerMiddleware: could not start profiling the request", e, backtrace: true)
|
|
46
46
|
subscribed = false
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -104,7 +104,9 @@ module Profiler
|
|
|
104
104
|
record_response(env, profile, request_body, status, headers, content, content.bytesize, true)
|
|
105
105
|
collect_all(profile, collectors)
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
# A profile that was not saved (a store unavailable, a save that failed) gets no token and
|
|
108
|
+
# no toolbar: both would point to a profile that is not there.
|
|
109
|
+
return [status, headers, body] unless Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
108
110
|
|
|
109
111
|
set_header(headers, TOKEN_HEADER, profile.token)
|
|
110
112
|
|
|
@@ -119,7 +121,7 @@ module Profiler
|
|
|
119
121
|
[status, headers, body]
|
|
120
122
|
rescue => e
|
|
121
123
|
# The application has run: its response goes out, profiled or not.
|
|
122
|
-
|
|
124
|
+
Profiler.log_error("ProfilerMiddleware: could not complete the profile", e, backtrace: true)
|
|
123
125
|
[status, headers, body || []]
|
|
124
126
|
end
|
|
125
127
|
|
|
@@ -137,7 +139,6 @@ module Profiler
|
|
|
137
139
|
collect_all(profile, now)
|
|
138
140
|
# Their tabs keep their place; what they hold is filled in when they are collected.
|
|
139
141
|
kept.each { |collector| profile.add_collector_metadata(collector) }
|
|
140
|
-
set_header(headers, TOKEN_HEADER, profile.token)
|
|
141
142
|
|
|
142
143
|
# A Rack 3 streaming body (call without each) writes to the socket itself: nothing to relay.
|
|
143
144
|
unless body.respond_to?(:each)
|
|
@@ -145,13 +146,18 @@ module Profiler
|
|
|
145
146
|
collector.collect if collector.respond_to?(:collect)
|
|
146
147
|
profile.refresh_collector_metadata(collector)
|
|
147
148
|
rescue => e
|
|
148
|
-
|
|
149
|
+
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
|
|
149
150
|
end
|
|
150
151
|
record_response(env, profile, request_body, status, profiled_headers, "", nil, false)
|
|
151
|
-
|
|
152
|
+
# Saved before the headers leave: a profile that is not there gets no token.
|
|
153
|
+
set_header(headers, TOKEN_HEADER, profile.token) if Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
152
154
|
return [[status, headers, body], nil]
|
|
153
155
|
end
|
|
154
156
|
|
|
157
|
+
# Saved when the server closes the body, after the headers have left: the token is sent
|
|
158
|
+
# unless the store is already known to be unavailable, where it would be dropped.
|
|
159
|
+
set_header(headers, TOKEN_HEADER, profile.token) unless Profiler.storage.is_a?(Profiler::Storage::Unavailable)
|
|
160
|
+
|
|
155
161
|
streamed = StreamedProfile.new(profile, kept) do |captured, size, complete, error|
|
|
156
162
|
finish_streamed(env, streamed, collectors, request_body, allocations_before, status, profiled_headers,
|
|
157
163
|
captured, size, complete, error)
|
|
@@ -163,7 +169,7 @@ module Profiler
|
|
|
163
169
|
StreamedProfile.register(streamed)
|
|
164
170
|
[[status, headers, streamed.body], kept]
|
|
165
171
|
rescue => e
|
|
166
|
-
|
|
172
|
+
Profiler.log_error("ProfilerMiddleware: could not profile a streamed response", e, backtrace: true)
|
|
167
173
|
[[status, headers, body], nil]
|
|
168
174
|
end
|
|
169
175
|
|
|
@@ -186,12 +192,12 @@ module Profiler
|
|
|
186
192
|
collector.collect
|
|
187
193
|
profile.refresh_collector_metadata(collector)
|
|
188
194
|
rescue => e
|
|
189
|
-
|
|
195
|
+
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
|
|
190
196
|
end
|
|
191
197
|
|
|
192
|
-
Profiler.
|
|
198
|
+
Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
193
199
|
rescue => e
|
|
194
|
-
|
|
200
|
+
Profiler.log_error("ProfilerMiddleware: could not finish the profile of a streamed response", e)
|
|
195
201
|
end
|
|
196
202
|
|
|
197
203
|
def collect_from_any_thread?(collector)
|
|
@@ -238,9 +244,9 @@ module Profiler
|
|
|
238
244
|
resp_content_type: ""
|
|
239
245
|
)
|
|
240
246
|
collect_all(profile, collectors)
|
|
241
|
-
Profiler.
|
|
247
|
+
Profiler.save_profile(profile, from: "ProfilerMiddleware")
|
|
242
248
|
rescue => e
|
|
243
|
-
|
|
249
|
+
Profiler.log_error("ProfilerMiddleware: could not record a failed request", e)
|
|
244
250
|
end
|
|
245
251
|
|
|
246
252
|
def collect_all(profile, collectors)
|
|
@@ -249,7 +255,7 @@ module Profiler
|
|
|
249
255
|
collector.collect if collector.respond_to?(:collect)
|
|
250
256
|
profile.add_collector_metadata(collector)
|
|
251
257
|
rescue => e
|
|
252
|
-
|
|
258
|
+
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
|
|
253
259
|
end
|
|
254
260
|
end
|
|
255
261
|
end
|
|
@@ -259,7 +265,7 @@ module Profiler
|
|
|
259
265
|
kept&.each do |collector|
|
|
260
266
|
collector.release_thread_slots if collector.respond_to?(:release_thread_slots)
|
|
261
267
|
rescue => e
|
|
262
|
-
|
|
268
|
+
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} release failed", e)
|
|
263
269
|
end
|
|
264
270
|
Profiler::CurrentContext.clear
|
|
265
271
|
end
|
|
@@ -376,7 +382,7 @@ module Profiler
|
|
|
376
382
|
begin
|
|
377
383
|
close_body(body)
|
|
378
384
|
rescue => e
|
|
379
|
-
|
|
385
|
+
Profiler.log_error("ProfilerMiddleware: closing a body that failed also failed", e)
|
|
380
386
|
end
|
|
381
387
|
raise
|
|
382
388
|
end
|
|
@@ -410,7 +416,7 @@ module Profiler
|
|
|
410
416
|
content = Redaction.cut_bytes(content, limit) if limit
|
|
411
417
|
{ content: content, size: size, size_is_minimum: size_is_minimum || false }
|
|
412
418
|
rescue => e
|
|
413
|
-
|
|
419
|
+
Profiler.log_error("ProfilerMiddleware: could not read the request body", e)
|
|
414
420
|
{ content: "", size: nil }
|
|
415
421
|
end
|
|
416
422
|
end
|
|
@@ -85,7 +85,7 @@ module Profiler
|
|
|
85
85
|
collector.collect if collector.respond_to?(:collect)
|
|
86
86
|
@profile.refresh_collector_metadata(collector)
|
|
87
87
|
rescue => e
|
|
88
|
-
|
|
88
|
+
Profiler.log_error("ProfilerMiddleware: collector #{collector.class} failed", e)
|
|
89
89
|
end
|
|
90
90
|
Collectors::Lifecycle.release_all(@collectors)
|
|
91
91
|
end
|
|
@@ -26,6 +26,17 @@ module Profiler
|
|
|
26
26
|
TAG_REST = %r{(?:#{WS}++|/|#{ATTRIBUTE})*+>}
|
|
27
27
|
# A whole start or end tag, read after its <: the slash and the name are captured.
|
|
28
28
|
TAG = %r{(/)?([a-z][^\t\n\f\r\ />]*+)#{TAG_REST}}i
|
|
29
|
+
# Text and complete tags that cannot hold the page's end, skipped in one match: every tag
|
|
30
|
+
# but </body>, a raw-text element, <plaintext>, a comment, a declaration or a tag left
|
|
31
|
+
# open, which the scan below reads one by one. A tag here is read with TAG_REST, as there.
|
|
32
|
+
NAME_END = "(?=[\\t\\n\\f\\r\\ />])"
|
|
33
|
+
PLAIN_MARKUP = %r{
|
|
34
|
+
(?:
|
|
35
|
+
[^<]++
|
|
36
|
+
| <(?:/(?!body#{NAME_END})|(?!(?:#{(RAW_TEXT_ELEMENTS.keys + [PLAINTEXT]).join("|")})#{NAME_END}))
|
|
37
|
+
[a-z][^\t\n\f\r\ />]*+#{TAG_REST}
|
|
38
|
+
)*+
|
|
39
|
+
}xi
|
|
29
40
|
|
|
30
41
|
def initialize(body, token, nonce = nil)
|
|
31
42
|
@body = body
|
|
@@ -58,7 +69,10 @@ module Profiler
|
|
|
58
69
|
scanner = StringScanner.new(bytes)
|
|
59
70
|
position = nil
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
loop do
|
|
73
|
+
skip_plain_markup(scanner)
|
|
74
|
+
break unless scanner.skip_until(/</)
|
|
75
|
+
|
|
62
76
|
start = scanner.pos - 1
|
|
63
77
|
|
|
64
78
|
if scanner.skip(TAG)
|
|
@@ -97,6 +111,11 @@ module Profiler
|
|
|
97
111
|
position
|
|
98
112
|
end
|
|
99
113
|
|
|
114
|
+
# A page is mostly ordinary text and tags: one match instead of one scan step per tag.
|
|
115
|
+
def skip_plain_markup(scanner)
|
|
116
|
+
scanner.skip(PLAIN_MARKUP)
|
|
117
|
+
end
|
|
118
|
+
|
|
100
119
|
# In a script, <!-- followed by <script enters a state where </script> does not close it.
|
|
101
120
|
# Only the script itself is searched, so that a page of many scripts is read in linear
|
|
102
121
|
# time.
|
|
@@ -124,7 +143,7 @@ module Profiler
|
|
|
124
143
|
path = File.join(__dir__, "..", "..", "..", "app", "assets", "javascript", "profiler-ajax-interceptor.js")
|
|
125
144
|
File.read(path)
|
|
126
145
|
rescue => e
|
|
127
|
-
|
|
146
|
+
Profiler.log_error("ToolbarInjector: could not load the AJAX interceptor script", e)
|
|
128
147
|
""
|
|
129
148
|
end
|
|
130
149
|
end
|
|
@@ -19,7 +19,7 @@ module Profiler
|
|
|
19
19
|
:response_body, :response_body_encoding,
|
|
20
20
|
:request_body_size, :request_body_truncated, :request_body_size_is_minimum,
|
|
21
21
|
:response_body_size, :response_body_truncated, :response_body_size_is_minimum,
|
|
22
|
-
:collectors_released_after_seconds,
|
|
22
|
+
:collectors_released_after_seconds, :params_truncated,
|
|
23
23
|
:gem_version
|
|
24
24
|
|
|
25
25
|
def initialize(request = nil)
|
|
@@ -34,7 +34,8 @@ module Profiler
|
|
|
34
34
|
if request
|
|
35
35
|
@path = request.path
|
|
36
36
|
@method = request.request_method
|
|
37
|
-
@params = sanitize_params(request.params)
|
|
37
|
+
@params, @params_truncated = cap_params(sanitize_params(request.params),
|
|
38
|
+
Profiler.configuration.max_captured_body_bytes)
|
|
38
39
|
@headers = extract_headers(request.env)
|
|
39
40
|
end
|
|
40
41
|
end
|
|
@@ -133,6 +134,7 @@ module Profiler
|
|
|
133
134
|
started_at: @started_at&.iso8601,
|
|
134
135
|
finished_at: @finished_at&.iso8601,
|
|
135
136
|
params: @params,
|
|
137
|
+
params_truncated: @params_truncated,
|
|
136
138
|
headers: @headers,
|
|
137
139
|
response_headers: @response_headers,
|
|
138
140
|
request_body: req_body,
|
|
@@ -178,6 +180,7 @@ module Profiler
|
|
|
178
180
|
profile.started_at = data[:started_at] ? Time.parse(data[:started_at]) : nil
|
|
179
181
|
profile.finished_at = data[:finished_at] ? Time.parse(data[:finished_at]) : nil
|
|
180
182
|
profile.params = data[:params]
|
|
183
|
+
profile.params_truncated = data[:params_truncated]
|
|
181
184
|
profile.headers = data[:headers]
|
|
182
185
|
profile.response_headers = data[:response_headers]
|
|
183
186
|
profile.request_body = data[:request_body]
|
|
@@ -266,6 +269,61 @@ module Profiler
|
|
|
266
269
|
|
|
267
270
|
LEGACY_FILTERED_PARAMS = %w[password password_confirmation token secret].freeze
|
|
268
271
|
|
|
272
|
+
# The key added to params cut at max_captured_body_bytes, so that the cut shows wherever
|
|
273
|
+
# the params are read.
|
|
274
|
+
PARAMS_TRUNCATED_KEY = "[profiler]"
|
|
275
|
+
|
|
276
|
+
# A copy of +params+ whose JSON stays within about +limit+ bytes: long strings are cut, and
|
|
277
|
+
# the entries past the limit left out. The application's own params are never touched.
|
|
278
|
+
# Returns the params and whether they were cut.
|
|
279
|
+
def cap_params(params, limit)
|
|
280
|
+
return [params, false] if limit.nil? || !params.is_a?(Hash)
|
|
281
|
+
|
|
282
|
+
budget = [limit]
|
|
283
|
+
cut = [false]
|
|
284
|
+
capped = cap_value(params, budget, cut)
|
|
285
|
+
return [capped, false] unless cut[0]
|
|
286
|
+
|
|
287
|
+
capped[PARAMS_TRUNCATED_KEY] = "params truncated at #{limit} bytes (max_captured_body_bytes)"
|
|
288
|
+
[capped, true]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def cap_value(value, budget, cut)
|
|
292
|
+
case value
|
|
293
|
+
when Hash
|
|
294
|
+
value.each_with_object({}) do |(key, item), out|
|
|
295
|
+
cost = key.to_s.bytesize + 4
|
|
296
|
+
if cost >= budget[0]
|
|
297
|
+
cut[0] = true
|
|
298
|
+
break out
|
|
299
|
+
end
|
|
300
|
+
budget[0] -= cost
|
|
301
|
+
out[key] = cap_value(item, budget, cut)
|
|
302
|
+
end
|
|
303
|
+
when Array
|
|
304
|
+
value.each_with_object([]) do |item, out|
|
|
305
|
+
if budget[0] <= 1
|
|
306
|
+
cut[0] = true
|
|
307
|
+
break out
|
|
308
|
+
end
|
|
309
|
+
budget[0] -= 1
|
|
310
|
+
out << cap_value(item, budget, cut)
|
|
311
|
+
end
|
|
312
|
+
when String
|
|
313
|
+
cost = value.bytesize + 2
|
|
314
|
+
if cost > budget[0]
|
|
315
|
+
cut[0] = true
|
|
316
|
+
value = Redaction.cut_bytes(value, [budget[0] - 2, 0].max).scrub("")
|
|
317
|
+
cost = value.bytesize + 2
|
|
318
|
+
end
|
|
319
|
+
budget[0] -= cost
|
|
320
|
+
value
|
|
321
|
+
else
|
|
322
|
+
budget[0] -= value.to_s.bytesize
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
269
327
|
def sanitize_params(params)
|
|
270
328
|
return {} unless params
|
|
271
329
|
|
|
@@ -36,7 +36,11 @@ module Profiler
|
|
|
36
36
|
candidates.find { |route| route[:verb].split("|").include?(verb) } ||
|
|
37
37
|
candidates.find { |route| route[:verb] == "ANY" } ||
|
|
38
38
|
candidates.first
|
|
39
|
-
rescue StandardError
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
# A path no route matches (a 404) has no route to show.
|
|
41
|
+
unless defined?(ActionController::RoutingError) && e.is_a?(ActionController::RoutingError)
|
|
42
|
+
Profiler.log_error_once(:snapshot_route, "could not find the route of a profile", e)
|
|
43
|
+
end
|
|
40
44
|
nil
|
|
41
45
|
end
|
|
42
46
|
|
|
@@ -61,7 +65,7 @@ module Profiler
|
|
|
61
65
|
end
|
|
62
66
|
profile
|
|
63
67
|
rescue StandardError => e
|
|
64
|
-
|
|
68
|
+
Profiler.log_error("could not add the routes and ENV to profile #{profile.token}", e)
|
|
65
69
|
profile
|
|
66
70
|
end
|
|
67
71
|
|
|
@@ -84,7 +88,8 @@ module Profiler
|
|
|
84
88
|
@routes = build_snapshot(fingerprint, list) unless @routes && @routes[0] == fingerprint
|
|
85
89
|
@routes
|
|
86
90
|
end
|
|
87
|
-
rescue StandardError
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Profiler.log_error_once(:snapshot_routes, "could not read the application's routes", e)
|
|
88
93
|
[nil, [].freeze, {}.freeze]
|
|
89
94
|
end
|
|
90
95
|
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -15,12 +15,15 @@ module Profiler
|
|
|
15
15
|
config.default(:enabled, development_or_test)
|
|
16
16
|
config.default(:storage, development_or_test ? :file : :memory)
|
|
17
17
|
config.default(:track_tests, Rails.env.test?)
|
|
18
|
+
# One test profile per example: no cap on the count in the test environment, so that the
|
|
19
|
+
# report at the end of a suite sees all of them.
|
|
20
|
+
config.default(:max_profiles, Rails.env.test? ? nil : 100)
|
|
18
21
|
|
|
19
22
|
app.config.profiler.each do |key, value|
|
|
20
23
|
if config.respond_to?("#{key}=")
|
|
21
24
|
config.public_send("#{key}=", value)
|
|
22
25
|
else
|
|
23
|
-
|
|
26
|
+
Profiler.log_warn("config.profiler.#{key} is not a profiler option, ignored")
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
data/lib/profiler/redaction.rb
CHANGED
|
@@ -518,9 +518,7 @@ module Profiler
|
|
|
518
518
|
return if reported.include?(error.class)
|
|
519
519
|
|
|
520
520
|
reported << error.class
|
|
521
|
-
|
|
522
|
-
logger = ::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
523
|
-
logger ? logger.warn(message) : Kernel.warn(message)
|
|
521
|
+
Profiler.log_warn("Redaction: #{error.class} while #{during}; the value was masked instead.")
|
|
524
522
|
rescue StandardError
|
|
525
523
|
nil
|
|
526
524
|
end
|
|
@@ -29,7 +29,8 @@ module Profiler
|
|
|
29
29
|
# The version of the last save of +token+, 0 when none is known or Redis cannot be read.
|
|
30
30
|
def version(token)
|
|
31
31
|
redis.get(key(token)).to_i
|
|
32
|
-
rescue StandardError
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Profiler.log_error_once(:redis_event_bus_version, "RedisEventBus: could not read the version of a profile", e)
|
|
33
34
|
0
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -34,7 +34,8 @@ module Profiler
|
|
|
34
34
|
|
|
35
35
|
def exists?(token)
|
|
36
36
|
!load(token).nil?
|
|
37
|
-
rescue
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
Profiler.log_error_once(:store_exists, "#{self.class.name.split("::").last}: could not look up a profile", e)
|
|
38
39
|
false
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -54,8 +55,9 @@ module Profiler
|
|
|
54
55
|
|
|
55
56
|
def broadcast_event(token)
|
|
56
57
|
Profiler::SSE.current.broadcast(token)
|
|
57
|
-
rescue StandardError
|
|
58
|
+
rescue StandardError => e
|
|
58
59
|
# Never let a broadcast failure prevent profile persistence
|
|
60
|
+
Profiler.log_error_once(:store_broadcast, "#{self.class.name.split("::").last}: could not announce a saved profile", e)
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
end
|