rails-profiler 0.1.4
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 +7 -0
- data/app/assets/builds/profiler-toolbar.js +1191 -0
- data/app/assets/builds/profiler.css +2668 -0
- data/app/assets/builds/profiler.js +2772 -0
- data/app/controllers/profiler/api/ajax_controller.rb +36 -0
- data/app/controllers/profiler/api/jobs_controller.rb +39 -0
- data/app/controllers/profiler/api/outbound_http_controller.rb +36 -0
- data/app/controllers/profiler/api/profiles_controller.rb +60 -0
- data/app/controllers/profiler/api/toolbar_controller.rb +44 -0
- data/app/controllers/profiler/application_controller.rb +19 -0
- data/app/controllers/profiler/assets_controller.rb +29 -0
- data/app/controllers/profiler/profiles_controller.rb +107 -0
- data/app/views/layouts/profiler/application.html.erb +16 -0
- data/app/views/layouts/profiler/embedded.html.erb +34 -0
- data/app/views/profiler/profiles/index.html.erb +1 -0
- data/app/views/profiler/profiles/show.html.erb +4 -0
- data/config/routes.rb +36 -0
- data/exe/profiler-mcp +8 -0
- data/lib/profiler/collectors/ajax_collector.rb +109 -0
- data/lib/profiler/collectors/base_collector.rb +92 -0
- data/lib/profiler/collectors/cache_collector.rb +96 -0
- data/lib/profiler/collectors/database_collector.rb +113 -0
- data/lib/profiler/collectors/dump_collector.rb +98 -0
- data/lib/profiler/collectors/flamegraph_collector.rb +182 -0
- data/lib/profiler/collectors/http_collector.rb +112 -0
- data/lib/profiler/collectors/job_collector.rb +50 -0
- data/lib/profiler/collectors/performance_collector.rb +103 -0
- data/lib/profiler/collectors/request_collector.rb +80 -0
- data/lib/profiler/collectors/view_collector.rb +79 -0
- data/lib/profiler/configuration.rb +81 -0
- data/lib/profiler/engine.rb +17 -0
- data/lib/profiler/instrumentation/active_job_instrumentation.rb +22 -0
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +153 -0
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +18 -0
- data/lib/profiler/job_profiler.rb +118 -0
- data/lib/profiler/mcp/resources/n1_patterns.rb +62 -0
- data/lib/profiler/mcp/resources/recent_jobs.rb +39 -0
- data/lib/profiler/mcp/resources/recent_requests.rb +35 -0
- data/lib/profiler/mcp/resources/slow_queries.rb +47 -0
- data/lib/profiler/mcp/server.rb +217 -0
- data/lib/profiler/mcp/tools/analyze_queries.rb +124 -0
- data/lib/profiler/mcp/tools/clear_profiles.rb +22 -0
- data/lib/profiler/mcp/tools/get_profile_ajax.rb +66 -0
- data/lib/profiler/mcp/tools/get_profile_detail.rb +326 -0
- data/lib/profiler/mcp/tools/get_profile_dumps.rb +51 -0
- data/lib/profiler/mcp/tools/get_profile_http.rb +104 -0
- data/lib/profiler/mcp/tools/query_jobs.rb +60 -0
- data/lib/profiler/mcp/tools/query_profiles.rb +66 -0
- data/lib/profiler/middleware/cors_middleware.rb +55 -0
- data/lib/profiler/middleware/profiler_middleware.rb +151 -0
- data/lib/profiler/middleware/toolbar_injector.rb +378 -0
- data/lib/profiler/models/profile.rb +182 -0
- data/lib/profiler/models/sql_query.rb +48 -0
- data/lib/profiler/models/timeline_event.rb +40 -0
- data/lib/profiler/railtie.rb +75 -0
- data/lib/profiler/storage/base_store.rb +41 -0
- data/lib/profiler/storage/blob_store.rb +46 -0
- data/lib/profiler/storage/file_store.rb +119 -0
- data/lib/profiler/storage/memory_store.rb +94 -0
- data/lib/profiler/storage/redis_store.rb +98 -0
- data/lib/profiler/storage/sqlite_store.rb +272 -0
- data/lib/profiler/tasks/profiler.rake +79 -0
- data/lib/profiler/version.rb +5 -0
- data/lib/profiler.rb +68 -0
- metadata +194 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class AjaxController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token, only: [:link]
|
|
7
|
+
|
|
8
|
+
def link
|
|
9
|
+
parent_token = params[:parent_token]
|
|
10
|
+
child_token = params[:child_token]
|
|
11
|
+
|
|
12
|
+
# Validate parameters
|
|
13
|
+
if parent_token.blank? || child_token.blank?
|
|
14
|
+
return render json: { error: "Missing parent_token or child_token" }, status: :bad_request
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load child profile
|
|
18
|
+
child_profile = Profiler.storage.load(child_token)
|
|
19
|
+
unless child_profile
|
|
20
|
+
return render json: { error: "Child profile not found" }, status: :not_found
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Set parent relationship
|
|
24
|
+
child_profile.parent_token = parent_token
|
|
25
|
+
child_profile.is_ajax = true
|
|
26
|
+
|
|
27
|
+
# Save updated profile
|
|
28
|
+
Profiler.storage.save(child_token, child_profile)
|
|
29
|
+
|
|
30
|
+
render json: { success: true }
|
|
31
|
+
rescue => e
|
|
32
|
+
render json: { error: e.message }, status: :internal_server_error
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class JobsController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
all_profiles = Profiler.storage.list(limit: (params[:limit] || 200).to_i, offset: (params[:offset] || 0).to_i)
|
|
10
|
+
job_profiles = all_profiles.select { |p| p.profile_type == "job" }
|
|
11
|
+
job_profiles = job_profiles.first((params[:limit] || 50).to_i)
|
|
12
|
+
render json: job_profiles.map(&:to_h)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
profile = Profiler.storage.load(params[:id])
|
|
17
|
+
|
|
18
|
+
unless profile && profile.profile_type == "job"
|
|
19
|
+
return render json: { error: "Job profile not found" }, status: :not_found
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
render json: profile.to_h
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def destroy
|
|
26
|
+
profile = Profiler.storage.load(params[:id])
|
|
27
|
+
return render json: { error: "Job profile not found" }, status: :not_found unless profile&.profile_type == "job"
|
|
28
|
+
|
|
29
|
+
Profiler.storage.delete(params[:id])
|
|
30
|
+
head :no_content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear
|
|
34
|
+
Profiler.storage.clear(type: "job")
|
|
35
|
+
head :no_content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class OutboundHttpController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
# GET /_profiler/api/outbound_http
|
|
9
|
+
def index
|
|
10
|
+
limit = (params[:limit] || 200).to_i
|
|
11
|
+
profiles = Profiler.storage.list(limit: limit)
|
|
12
|
+
|
|
13
|
+
requests = profiles.flat_map do |profile|
|
|
14
|
+
http_data = profile.collector_data("http")
|
|
15
|
+
next [] unless http_data && http_data["requests"]&.any?
|
|
16
|
+
|
|
17
|
+
http_data["requests"]
|
|
18
|
+
.reject { |req| profiler_url?(req["url"].to_s) }
|
|
19
|
+
.map { |req| req.merge("profile_token" => profile.token, "profile_started_at" => profile.started_at) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
requests.sort_by! { |r| r["profile_started_at"] }.reverse!
|
|
23
|
+
|
|
24
|
+
render json: requests
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def profiler_url?(url)
|
|
30
|
+
URI.parse(url).path.start_with?("/_profiler")
|
|
31
|
+
rescue URI::InvalidURIError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class ProfilesController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
profiles = Profiler.storage.list(limit: params[:limit] || 50, offset: params[:offset] || 0)
|
|
10
|
+
render json: profiles.map(&:to_h)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
profile = Profiler.storage.load(params[:id])
|
|
15
|
+
|
|
16
|
+
unless profile
|
|
17
|
+
return render json: { error: "Profile not found" }, status: :not_found
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
21
|
+
recalculate_ajax_data(profile)
|
|
22
|
+
|
|
23
|
+
render json: profile.to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def destroy
|
|
27
|
+
profile = Profiler.storage.load(params[:id])
|
|
28
|
+
return render json: { error: "Profile not found" }, status: :not_found unless profile
|
|
29
|
+
|
|
30
|
+
Profiler.storage.delete(params[:id])
|
|
31
|
+
head :no_content
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
Profiler.storage.clear(type: "http")
|
|
36
|
+
head :no_content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def recalculate_ajax_data(profile)
|
|
42
|
+
# Find AJAX collector in the configured collectors
|
|
43
|
+
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
44
|
+
|
|
45
|
+
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
46
|
+
collector = ajax_collector_class.new(profile)
|
|
47
|
+
collector.collect
|
|
48
|
+
|
|
49
|
+
# Update tab metadata to reflect has_data status
|
|
50
|
+
if profile.instance_variable_get(:@collectors_metadata)
|
|
51
|
+
ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
|
|
52
|
+
if ajax_tab
|
|
53
|
+
ajax_tab[:has_data] = collector.has_data?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class ToolbarController < Profiler::ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
skip_before_action :check_authorization
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
profile = Profiler.storage.load(params[:token])
|
|
11
|
+
|
|
12
|
+
unless profile
|
|
13
|
+
render json: { error: "Profile not found" }, status: :not_found
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
18
|
+
recalculate_ajax_data(profile)
|
|
19
|
+
|
|
20
|
+
render json: { profile: profile.to_h }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def recalculate_ajax_data(profile)
|
|
26
|
+
# Find AJAX collector in the configured collectors
|
|
27
|
+
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
28
|
+
|
|
29
|
+
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
30
|
+
collector = ajax_collector_class.new(profile)
|
|
31
|
+
collector.collect
|
|
32
|
+
|
|
33
|
+
# Update tab metadata to reflect has_data status
|
|
34
|
+
if profile.instance_variable_get(:@collectors_metadata)
|
|
35
|
+
ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
|
|
36
|
+
if ajax_tab
|
|
37
|
+
ajax_tab[:has_data] = collector.has_data?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
|
|
7
|
+
layout "profiler/application"
|
|
8
|
+
|
|
9
|
+
before_action :check_authorization
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def check_authorization
|
|
14
|
+
unless Profiler.configuration.enabled
|
|
15
|
+
render plain: "Profiler is disabled", status: :forbidden
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
class AssetsController < ApplicationController
|
|
5
|
+
skip_before_action :verify_authenticity_token
|
|
6
|
+
skip_before_action :check_authorization
|
|
7
|
+
|
|
8
|
+
def toolbar_js
|
|
9
|
+
path = Profiler::Engine.root.join("app", "assets", "builds", "profiler-toolbar.js")
|
|
10
|
+
js = File.read(path)
|
|
11
|
+
expires_in 1.hour, public: true
|
|
12
|
+
render plain: js, content_type: "application/javascript"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def main_js
|
|
16
|
+
path = Profiler::Engine.root.join("app", "assets", "builds", "profiler.js")
|
|
17
|
+
js = File.read(path)
|
|
18
|
+
expires_in 1.hour, public: true
|
|
19
|
+
render plain: js, content_type: "application/javascript"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def main_css
|
|
23
|
+
path = Profiler::Engine.root.join("app", "assets", "builds", "profiler.css")
|
|
24
|
+
css = File.read(path)
|
|
25
|
+
expires_in 1.hour, public: true
|
|
26
|
+
render plain: css, content_type: "text/css"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
class ProfilesController < ApplicationController
|
|
5
|
+
before_action :allow_iframe_embedding, only: [:show], if: -> { params[:embed] == "true" }
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
limit = params[:limit]&.to_i || 50
|
|
9
|
+
offset = params[:offset]&.to_i || 0
|
|
10
|
+
|
|
11
|
+
@profiles = Profiler.storage.list(limit: limit, offset: offset)
|
|
12
|
+
@profiles = filter_profiles(@profiles) if params[:filter].present?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
@profile = Profiler.storage.load(params[:id])
|
|
17
|
+
|
|
18
|
+
unless @profile
|
|
19
|
+
render plain: "Profile not found", status: :not_found
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
24
|
+
recalculate_ajax_data(@profile)
|
|
25
|
+
|
|
26
|
+
@embedded = params[:embed] == "true"
|
|
27
|
+
|
|
28
|
+
render layout: @embedded ? "profiler/embedded" : "profiler/application"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def timeline
|
|
32
|
+
@profile = Profiler.storage.load(params[:id])
|
|
33
|
+
render json: @profile.collector_data("performance")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def database
|
|
37
|
+
@profile = Profiler.storage.load(params[:id])
|
|
38
|
+
render json: @profile.collector_data("database")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def views
|
|
42
|
+
@profile = Profiler.storage.load(params[:id])
|
|
43
|
+
render json: @profile.collector_data("view")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cache
|
|
47
|
+
@profile = Profiler.storage.load(params[:id])
|
|
48
|
+
render json: @profile.collector_data("cache")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def performance
|
|
52
|
+
@profile = Profiler.storage.load(params[:id])
|
|
53
|
+
render json: @profile.collector_data("performance")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def flamegraph
|
|
57
|
+
@profile = Profiler.storage.load(params[:id])
|
|
58
|
+
render json: @profile.collector_data("flamegraph")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def recalculate_ajax_data(profile)
|
|
64
|
+
# Find AJAX collector in the configured collectors
|
|
65
|
+
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
66
|
+
|
|
67
|
+
if Profiler.configuration.collectors.include?(ajax_collector_class)
|
|
68
|
+
collector = ajax_collector_class.new(profile)
|
|
69
|
+
collector.collect
|
|
70
|
+
|
|
71
|
+
# Update tab metadata to reflect has_data status
|
|
72
|
+
if profile.instance_variable_get(:@collectors_metadata)
|
|
73
|
+
ajax_tab = profile.instance_variable_get(:@collectors_metadata).find { |tab| tab[:key] == 'ajax' }
|
|
74
|
+
if ajax_tab
|
|
75
|
+
ajax_tab[:has_data] = collector.has_data?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def allow_iframe_embedding
|
|
82
|
+
response.headers.delete('X-Frame-Options')
|
|
83
|
+
# Don't set frame-ancestors CSP to allow Chrome extensions to embed
|
|
84
|
+
# Mark that CSP should not be set by middleware
|
|
85
|
+
request.env['profiler.skip_csp'] = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def filter_profiles(profiles)
|
|
89
|
+
filtered = profiles
|
|
90
|
+
|
|
91
|
+
if params[:filter][:path].present?
|
|
92
|
+
filtered = filtered.select { |p| p.path.include?(params[:filter][:path]) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if params[:filter][:method].present?
|
|
96
|
+
filtered = filtered.select { |p| p.method == params[:filter][:method] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if params[:filter][:min_duration].present?
|
|
100
|
+
min = params[:filter][:min_duration].to_f
|
|
101
|
+
filtered = filtered.select { |p| p.duration >= min }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
filtered
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Rails Profiler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<link rel="stylesheet" href="/_profiler/assets/profiler.css">
|
|
10
|
+
<script src="/_profiler/assets/profiler.js" defer></script>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<%= yield %>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-theme="<%= params[:theme].presence&.then { |t| %w[light dark].include?(t) ? t : 'dark' } || 'dark' %>">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Rails Profiler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<link rel="stylesheet" href="/_profiler/assets/profiler.css">
|
|
10
|
+
<script src="/_profiler/assets/profiler.js" defer></script>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
/* Embedded overrides — tighter layout, no page chrome */
|
|
14
|
+
body { overflow-y: auto; }
|
|
15
|
+
.container { padding: 16px 20px; }
|
|
16
|
+
</style>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
// Receive theme changes from the Chrome extension panel via postMessage.
|
|
20
|
+
// The extension sends: { type: 'profiler:set-theme', theme: 'light' | 'dark' }
|
|
21
|
+
window.addEventListener('message', function(e) {
|
|
22
|
+
var data = e.data;
|
|
23
|
+
if (data && data.type === 'profiler:set-theme' &&
|
|
24
|
+
(data.theme === 'light' || data.theme === 'dark')) {
|
|
25
|
+
document.documentElement.setAttribute('data-theme', data.theme);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</head>
|
|
30
|
+
|
|
31
|
+
<body>
|
|
32
|
+
<%= yield %>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div id="profiler-index"></div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "profiler/mcp/server"
|
|
4
|
+
|
|
5
|
+
Profiler::Engine.routes.draw do
|
|
6
|
+
mount Profiler::MCP::Server.rack_app, at: "mcp"
|
|
7
|
+
|
|
8
|
+
root to: "profiles#index"
|
|
9
|
+
|
|
10
|
+
resources :profiles, only: [:index, :show] do
|
|
11
|
+
member do
|
|
12
|
+
get :timeline
|
|
13
|
+
get :database
|
|
14
|
+
get :views
|
|
15
|
+
get :cache
|
|
16
|
+
get :performance
|
|
17
|
+
get :flamegraph
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
get "assets/profiler-toolbar.js", to: "assets#toolbar_js"
|
|
22
|
+
get "assets/profiler.js", to: "assets#main_js"
|
|
23
|
+
get "assets/profiler.css", to: "assets#main_css"
|
|
24
|
+
|
|
25
|
+
namespace :api do
|
|
26
|
+
resources :profiles, only: [:index, :show, :destroy] do
|
|
27
|
+
collection { delete :clear }
|
|
28
|
+
end
|
|
29
|
+
resources :jobs, only: [:index, :show, :destroy] do
|
|
30
|
+
collection { delete :clear }
|
|
31
|
+
end
|
|
32
|
+
resources :outbound_http, only: [:index]
|
|
33
|
+
get "toolbar/:token", to: "toolbar#show"
|
|
34
|
+
post "ajax/link", to: "ajax#link"
|
|
35
|
+
end
|
|
36
|
+
end
|
data/exe/profiler-mcp
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_collector"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
module Collectors
|
|
7
|
+
class AjaxCollector < BaseCollector
|
|
8
|
+
def icon
|
|
9
|
+
"🌐"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def priority
|
|
13
|
+
25
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tab_config
|
|
17
|
+
{
|
|
18
|
+
key: "ajax",
|
|
19
|
+
label: "AJAX",
|
|
20
|
+
icon: icon,
|
|
21
|
+
priority: priority,
|
|
22
|
+
enabled: true,
|
|
23
|
+
default_active: false
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def subscribe
|
|
28
|
+
# Passive collector - no subscriptions needed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collect
|
|
32
|
+
# Query storage for child AJAX profiles
|
|
33
|
+
ajax_profiles = Profiler.storage.find_by_parent(@profile.token)
|
|
34
|
+
|
|
35
|
+
return store_data({}) if ajax_profiles.empty?
|
|
36
|
+
|
|
37
|
+
# Generate summary statistics
|
|
38
|
+
data = {
|
|
39
|
+
"total_requests" => ajax_profiles.size,
|
|
40
|
+
"total_duration" => ajax_profiles.sum(&:duration).round(2),
|
|
41
|
+
"by_method" => group_by_method(ajax_profiles),
|
|
42
|
+
"by_status" => group_by_status(ajax_profiles),
|
|
43
|
+
"requests" => ajax_profiles.map { |p| request_summary(p) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
store_data(data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def toolbar_summary
|
|
50
|
+
return "" unless @data && @data["total_requests"]&.positive?
|
|
51
|
+
|
|
52
|
+
total = @data["total_requests"]
|
|
53
|
+
duration = @data["total_duration"]
|
|
54
|
+
|
|
55
|
+
# Color coding based on number of requests
|
|
56
|
+
color = if total > 20
|
|
57
|
+
"orange"
|
|
58
|
+
elsif total > 50
|
|
59
|
+
"red"
|
|
60
|
+
else
|
|
61
|
+
"green"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
text: "#{total} AJAX (#{duration}ms)",
|
|
66
|
+
color: color
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def group_by_method(profiles)
|
|
73
|
+
profiles.group_by(&:method).transform_values(&:count)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def group_by_status(profiles)
|
|
77
|
+
profiles.group_by { |p| status_category(p.status) }.transform_values(&:count)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def status_category(status)
|
|
81
|
+
return "unknown" unless status
|
|
82
|
+
|
|
83
|
+
case status
|
|
84
|
+
when 200..299
|
|
85
|
+
"2xx"
|
|
86
|
+
when 300..399
|
|
87
|
+
"3xx"
|
|
88
|
+
when 400..499
|
|
89
|
+
"4xx"
|
|
90
|
+
when 500..599
|
|
91
|
+
"5xx"
|
|
92
|
+
else
|
|
93
|
+
"other"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def request_summary(profile)
|
|
98
|
+
{
|
|
99
|
+
"token" => profile.token,
|
|
100
|
+
"path" => profile.path,
|
|
101
|
+
"method" => profile.method,
|
|
102
|
+
"status" => profile.status,
|
|
103
|
+
"duration" => profile.duration,
|
|
104
|
+
"started_at" => profile.started_at&.iso8601
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Collectors
|
|
5
|
+
class BaseCollector
|
|
6
|
+
attr_reader :profile
|
|
7
|
+
|
|
8
|
+
def initialize(profile)
|
|
9
|
+
@profile = profile
|
|
10
|
+
@data = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
self.class.name.split("::").last.gsub("Collector", "").downcase
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def icon
|
|
18
|
+
"📊"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def priority
|
|
22
|
+
100
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Tab configuration for dynamic tab system
|
|
26
|
+
def tab_config
|
|
27
|
+
{
|
|
28
|
+
key: name,
|
|
29
|
+
label: name.capitalize,
|
|
30
|
+
icon: icon,
|
|
31
|
+
priority: priority,
|
|
32
|
+
enabled: true,
|
|
33
|
+
default_active: false
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# How to render the tab: :auto (use JSON), :custom (call render_html), :client (frontend renderer)
|
|
38
|
+
def render_mode
|
|
39
|
+
:auto
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# For custom HTML rendering from backend (when render_mode is :custom)
|
|
43
|
+
def render_html(profile)
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether this collector has data for the current profile
|
|
48
|
+
def has_data?
|
|
49
|
+
data = panel_content
|
|
50
|
+
return false if data.nil?
|
|
51
|
+
return false if data.is_a?(Hash) && data.empty?
|
|
52
|
+
return false if data.is_a?(Array) && data.empty?
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def subscribe
|
|
57
|
+
# Override in subclasses to subscribe to ActiveSupport::Notifications
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def collect
|
|
61
|
+
# Override in subclasses to collect data
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def toolbar_summary
|
|
65
|
+
# Override in subclasses to provide summary for toolbar
|
|
66
|
+
""
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def panel_content
|
|
70
|
+
# Override in subclasses to provide full panel content
|
|
71
|
+
@data
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.inherited(subclass)
|
|
75
|
+
super
|
|
76
|
+
# Auto-register collectors
|
|
77
|
+
(@descendants ||= []) << subclass
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.descendants
|
|
81
|
+
@descendants || []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
|
|
86
|
+
def store_data(data)
|
|
87
|
+
@data = data
|
|
88
|
+
@profile.add_collector_data(name, data)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|