rails-profiler 0.10.1 → 0.11.1
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/app/assets/builds/profiler.css +6 -0
- data/app/assets/builds/profiler.js +44 -10
- data/app/controllers/profiler/api/jobs_controller.rb +11 -4
- data/app/controllers/profiler/api/profiles_controller.rb +11 -2
- data/lib/profiler/configuration.rb +15 -0
- data/lib/profiler/mcp/body_formatter.rb +12 -1
- data/lib/profiler/mcp/file_cache.rb +7 -4
- data/lib/profiler/models/profile.rb +17 -4
- data/lib/profiler/railtie.rb +1 -3
- data/lib/profiler/storage/file_store.rb +1 -5
- data/lib/profiler/storage/sqlite_store.rb +2 -10
- data/lib/profiler/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5895b7dd1e9141516af0f2a710c2eac52a458d6dcc6e7e8c0847577bb7dd1429
|
|
4
|
+
data.tar.gz: fd4c1d48bee719dff9a14b658389a2e0cad306a274cae92db4cf2ed0c4ddb11d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aea2caa0bde982cca5fc88a4fe7045b4600e0b293442477e105300514480a113ba8a93a10f568ee74ec2f21534c8c4e283dd70558392024a12f1219e38b31a72
|
|
7
|
+
data.tar.gz: 2a88a395c11f73907e383fd7b836f4f6c77974aed00c254fe3b586b77d81b7ab15c166641518642f91c39c7356c76d6111ab45bbd89eade95c05ed558c3e9077
|
|
@@ -740,7 +740,13 @@
|
|
|
740
740
|
};
|
|
741
741
|
const [section, setSection] = d2(initialSection);
|
|
742
742
|
const [profiles, setProfiles] = d2([]);
|
|
743
|
+
const [httpOffset, setHttpOffset] = d2(0);
|
|
744
|
+
const [httpHasMore, setHttpHasMore] = d2(false);
|
|
745
|
+
const [httpLoadingMore, setHttpLoadingMore] = d2(false);
|
|
743
746
|
const [jobs, setJobs] = d2([]);
|
|
747
|
+
const [jobOffset, setJobOffset] = d2(0);
|
|
748
|
+
const [jobHasMore, setJobHasMore] = d2(false);
|
|
749
|
+
const [jobLoadingMore, setJobLoadingMore] = d2(false);
|
|
744
750
|
const [outboundRequests, setOutboundRequests] = d2([]);
|
|
745
751
|
const [loadingHttp, setLoadingHttp] = d2(true);
|
|
746
752
|
const [loadingJobs, setLoadingJobs] = d2(false);
|
|
@@ -767,8 +773,10 @@
|
|
|
767
773
|
const [outboundMethod, setOutboundMethod] = d2("");
|
|
768
774
|
const [outboundStatus, setOutboundStatus] = d2("");
|
|
769
775
|
y2(() => {
|
|
770
|
-
fetch(`${BASE}/api/profiles`).then((res) => res.json()).then((data) => {
|
|
771
|
-
setProfiles(data.
|
|
776
|
+
fetch(`${BASE}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
777
|
+
setProfiles(data.profiles);
|
|
778
|
+
setHttpOffset(data.profiles.length);
|
|
779
|
+
setHttpHasMore(data.has_more);
|
|
772
780
|
setLoadingHttp(false);
|
|
773
781
|
}).catch(() => {
|
|
774
782
|
setError("Failed to load profiles");
|
|
@@ -803,11 +811,22 @@
|
|
|
803
811
|
const togglePreset = (key) => {
|
|
804
812
|
setHttpPreset((prev) => prev === key ? "" : key);
|
|
805
813
|
};
|
|
814
|
+
const loadMoreHttp = () => {
|
|
815
|
+
setHttpLoadingMore(true);
|
|
816
|
+
fetch(`${BASE}/api/profiles?limit=50&offset=${httpOffset}`).then((res) => res.json()).then((data) => {
|
|
817
|
+
setProfiles((prev) => [...prev, ...data.profiles]);
|
|
818
|
+
setHttpOffset((prev) => prev + data.profiles.length);
|
|
819
|
+
setHttpHasMore(data.has_more);
|
|
820
|
+
setHttpLoadingMore(false);
|
|
821
|
+
}).catch(() => setHttpLoadingMore(false));
|
|
822
|
+
};
|
|
806
823
|
const loadJobs = () => {
|
|
807
824
|
if (jobsLoaded) return;
|
|
808
825
|
setLoadingJobs(true);
|
|
809
|
-
fetch(`${BASE}/api/jobs`).then((res) => res.json()).then((data) => {
|
|
810
|
-
setJobs(data);
|
|
826
|
+
fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
827
|
+
setJobs(data.profiles);
|
|
828
|
+
setJobOffset(data.profiles.length);
|
|
829
|
+
setJobHasMore(data.has_more);
|
|
811
830
|
setLoadingJobs(false);
|
|
812
831
|
setJobsLoaded(true);
|
|
813
832
|
}).catch(() => {
|
|
@@ -816,6 +835,15 @@
|
|
|
816
835
|
setJobsLoaded(true);
|
|
817
836
|
});
|
|
818
837
|
};
|
|
838
|
+
const loadMoreJobs = () => {
|
|
839
|
+
setJobLoadingMore(true);
|
|
840
|
+
fetch(`${BASE}/api/jobs?limit=50&offset=${jobOffset}`).then((res) => res.json()).then((data) => {
|
|
841
|
+
setJobs((prev) => [...prev, ...data.profiles]);
|
|
842
|
+
setJobOffset((prev) => prev + data.profiles.length);
|
|
843
|
+
setJobHasMore(data.has_more);
|
|
844
|
+
setJobLoadingMore(false);
|
|
845
|
+
}).catch(() => setJobLoadingMore(false));
|
|
846
|
+
};
|
|
819
847
|
const loadOutbound = () => {
|
|
820
848
|
if (outboundLoaded) return;
|
|
821
849
|
setLoadingOutbound(true);
|
|
@@ -878,8 +906,10 @@
|
|
|
878
906
|
const refresh = () => {
|
|
879
907
|
if (section === "http") {
|
|
880
908
|
setLoadingHttp(true);
|
|
881
|
-
fetch(`${BASE}/api/profiles`).then((res) => res.json()).then((data) => {
|
|
882
|
-
setProfiles(data.
|
|
909
|
+
fetch(`${BASE}/api/profiles?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
910
|
+
setProfiles(data.profiles);
|
|
911
|
+
setHttpOffset(data.profiles.length);
|
|
912
|
+
setHttpHasMore(data.has_more);
|
|
883
913
|
setLoadingHttp(false);
|
|
884
914
|
}).catch(() => {
|
|
885
915
|
setError("Failed to load profiles");
|
|
@@ -887,8 +917,10 @@
|
|
|
887
917
|
});
|
|
888
918
|
} else if (section === "jobs") {
|
|
889
919
|
setLoadingJobs(true);
|
|
890
|
-
fetch(`${BASE}/api/jobs`).then((res) => res.json()).then((data) => {
|
|
891
|
-
setJobs(data);
|
|
920
|
+
fetch(`${BASE}/api/jobs?limit=50&offset=0`).then((res) => res.json()).then((data) => {
|
|
921
|
+
setJobs(data.profiles);
|
|
922
|
+
setJobOffset(data.profiles.length);
|
|
923
|
+
setJobHasMore(data.has_more);
|
|
892
924
|
setLoadingJobs(false);
|
|
893
925
|
}).catch(() => {
|
|
894
926
|
setJobsError("Failed to load job profiles");
|
|
@@ -1094,7 +1126,8 @@
|
|
|
1094
1126
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
1095
1127
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteProfile(p3.token), title: "Delete", children: "\xD7" }) })
|
|
1096
1128
|
] }, p3.token)) })
|
|
1097
|
-
] })
|
|
1129
|
+
] }),
|
|
1130
|
+
httpHasMore && !httpFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreHttp, disabled: httpLoadingMore, children: httpLoadingMore ? "Loading\u2026" : "Load more" }) })
|
|
1098
1131
|
] })),
|
|
1099
1132
|
section === "jobs" && (loadingJobs ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "Loading..." }) }) : jobsError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: jobsError }) }) : jobs.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
|
|
1100
1133
|
/* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No job profiles found" }),
|
|
@@ -1162,7 +1195,8 @@
|
|
|
1162
1195
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteJob(p3.token), title: "Delete", children: "\xD7" }) })
|
|
1163
1196
|
] }, p3.token);
|
|
1164
1197
|
}) })
|
|
1165
|
-
] })
|
|
1198
|
+
] }),
|
|
1199
|
+
jobHasMore && !jobFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreJobs, disabled: jobLoadingMore, children: jobLoadingMore ? "Loading\u2026" : "Load more" }) })
|
|
1166
1200
|
] })),
|
|
1167
1201
|
section === "outbound" && (loadingOutbound ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "Loading..." }) }) : outboundError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: outboundError }) }) : outboundRequests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
|
|
1168
1202
|
/* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No outbound HTTP requests found" }),
|
|
@@ -6,10 +6,17 @@ module Profiler
|
|
|
6
6
|
skip_before_action :verify_authenticity_token
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
limit = (params[:limit] || 50).to_i
|
|
10
|
+
offset = (params[:offset] || 0).to_i
|
|
11
|
+
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
12
|
+
jobs = all.select { |p| p.profile_type == "job" }
|
|
13
|
+
page = jobs.drop(offset).first(limit + 1)
|
|
14
|
+
render json: {
|
|
15
|
+
profiles: page.first(limit).map(&:to_h),
|
|
16
|
+
limit: limit,
|
|
17
|
+
offset: offset,
|
|
18
|
+
has_more: page.size > limit
|
|
19
|
+
}
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def show
|
|
@@ -6,8 +6,17 @@ module Profiler
|
|
|
6
6
|
skip_before_action :verify_authenticity_token
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
limit = (params[:limit] || 50).to_i
|
|
10
|
+
offset = (params[:offset] || 0).to_i
|
|
11
|
+
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
12
|
+
http = all.reject { |p| p.profile_type == "job" }
|
|
13
|
+
page = http.drop(offset).first(limit + 1)
|
|
14
|
+
render json: {
|
|
15
|
+
profiles: page.first(limit).map(&:to_h),
|
|
16
|
+
limit: limit,
|
|
17
|
+
offset: offset,
|
|
18
|
+
has_more: page.size > limit
|
|
19
|
+
}
|
|
11
20
|
end
|
|
12
21
|
|
|
13
22
|
def show
|
|
@@ -13,6 +13,8 @@ module Profiler
|
|
|
13
13
|
:track_jobs,
|
|
14
14
|
:compress_bodies, :compress_body_threshold
|
|
15
15
|
|
|
16
|
+
attr_writer :tmp_path
|
|
17
|
+
|
|
16
18
|
attr_reader :authorize_block
|
|
17
19
|
|
|
18
20
|
def initialize
|
|
@@ -41,6 +43,11 @@ module Profiler
|
|
|
41
43
|
@track_jobs = true
|
|
42
44
|
@compress_bodies = true
|
|
43
45
|
@compress_body_threshold = 10 * 1024 # 10 KB
|
|
46
|
+
@tmp_path = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tmp_path
|
|
50
|
+
@tmp_path || default_tmp_path
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
def authorize_with(&block)
|
|
@@ -64,6 +71,14 @@ module Profiler
|
|
|
64
71
|
|
|
65
72
|
private
|
|
66
73
|
|
|
74
|
+
def default_tmp_path
|
|
75
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
76
|
+
Rails.root.join("tmp", "rails-profiler")
|
|
77
|
+
else
|
|
78
|
+
File.expand_path("tmp/rails-profiler", Dir.pwd)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
67
82
|
def build_storage_backend
|
|
68
83
|
case @storage
|
|
69
84
|
when :memory
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "base64"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
3
6
|
module Profiler
|
|
4
7
|
module MCP
|
|
5
8
|
module BodyFormatter
|
|
6
9
|
# Formats a body string for MCP output.
|
|
7
10
|
#
|
|
8
11
|
# params keys used:
|
|
9
|
-
# "save_bodies" (boolean) — save to
|
|
12
|
+
# "save_bodies" (boolean) — save to ./tmp/rails-profiler/{token}/{name} and return path
|
|
10
13
|
# "max_body_size" (number) — truncate inline body at N chars
|
|
11
14
|
# "json_path" (string) — JSONPath to extract from body when save_bodies is true
|
|
12
15
|
# "xml_path" (string) — XPath to extract from body when save_bodies is true
|
|
@@ -16,6 +19,14 @@ module Profiler
|
|
|
16
19
|
return " *(binary, base64 encoded)*" if encoding == "base64"
|
|
17
20
|
return nil if body.nil? || body.empty?
|
|
18
21
|
|
|
22
|
+
if encoding == "gzip+base64"
|
|
23
|
+
begin
|
|
24
|
+
body = Zlib::Inflate.inflate(Base64.strict_decode64(body))
|
|
25
|
+
rescue Zlib::Error, ArgumentError
|
|
26
|
+
# fall through with original body
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
19
30
|
if params["save_bodies"]
|
|
20
31
|
path = FileCache.save(token, name, body)
|
|
21
32
|
if path
|
|
@@ -5,12 +5,14 @@ require "fileutils"
|
|
|
5
5
|
module Profiler
|
|
6
6
|
module MCP
|
|
7
7
|
class FileCache
|
|
8
|
-
|
|
8
|
+
def self.base_dir
|
|
9
|
+
Profiler.configuration.tmp_path.to_s
|
|
10
|
+
end
|
|
9
11
|
|
|
10
12
|
def self.save(token, name, content)
|
|
11
13
|
cleanup if rand < 0.05
|
|
12
14
|
|
|
13
|
-
dir = File.join(
|
|
15
|
+
dir = File.join(base_dir, token)
|
|
14
16
|
FileUtils.mkdir_p(dir)
|
|
15
17
|
path = File.join(dir, name)
|
|
16
18
|
File.write(path, content)
|
|
@@ -20,9 +22,10 @@ module Profiler
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def self.cleanup(max_age: 3600)
|
|
23
|
-
|
|
25
|
+
bd = base_dir
|
|
26
|
+
return unless Dir.exist?(bd)
|
|
24
27
|
|
|
25
|
-
Dir.glob(File.join(
|
|
28
|
+
Dir.glob(File.join(bd, "*")).each do |dir|
|
|
26
29
|
FileUtils.rm_rf(dir) if File.directory?(dir) && (Time.now - File.mtime(dir)) > max_age
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -74,6 +74,9 @@ module Profiler
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def to_h
|
|
77
|
+
req_body, req_enc = decode_body(@request_body, @request_body_encoding)
|
|
78
|
+
resp_body, resp_enc = decode_body(@response_body, @response_body_encoding)
|
|
79
|
+
|
|
77
80
|
{
|
|
78
81
|
profile_type: @profile_type,
|
|
79
82
|
token: @token,
|
|
@@ -87,10 +90,10 @@ module Profiler
|
|
|
87
90
|
params: @params,
|
|
88
91
|
headers: @headers,
|
|
89
92
|
response_headers: @response_headers,
|
|
90
|
-
request_body:
|
|
91
|
-
request_body_encoding:
|
|
92
|
-
response_body:
|
|
93
|
-
response_body_encoding:
|
|
93
|
+
request_body: req_body,
|
|
94
|
+
request_body_encoding: req_enc,
|
|
95
|
+
response_body: resp_body,
|
|
96
|
+
response_body_encoding: resp_enc,
|
|
94
97
|
collectors_data: @collectors_data,
|
|
95
98
|
tabs: @collectors_metadata,
|
|
96
99
|
parent_token: @parent_token,
|
|
@@ -173,6 +176,16 @@ module Profiler
|
|
|
173
176
|
text.bytesize > Profiler.configuration.compress_body_threshold
|
|
174
177
|
end
|
|
175
178
|
|
|
179
|
+
def decode_body(body, encoding)
|
|
180
|
+
return [body, encoding] unless encoding == "gzip+base64"
|
|
181
|
+
return [body, encoding] if body.nil? || body.empty?
|
|
182
|
+
|
|
183
|
+
decoded = Zlib::Inflate.inflate(Base64.strict_decode64(body))
|
|
184
|
+
[decoded, "text"]
|
|
185
|
+
rescue Zlib::Error, ArgumentError
|
|
186
|
+
[body, encoding]
|
|
187
|
+
end
|
|
188
|
+
|
|
176
189
|
def binary_content_type?(ct)
|
|
177
190
|
ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
|
|
178
191
|
end
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -11,9 +11,7 @@ module Profiler
|
|
|
11
11
|
Profiler.configure do |config|
|
|
12
12
|
config.enabled = Rails.env.development? || Rails.env.test?
|
|
13
13
|
config.storage = Rails.env.development? ? :file : :memory
|
|
14
|
-
config.
|
|
15
|
-
path: Rails.root.join("tmp", "profiler")
|
|
16
|
-
}
|
|
14
|
+
config.tmp_path = Rails.root.join("tmp", "rails-profiler")
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
17
|
|
|
@@ -253,19 +253,11 @@ module Profiler
|
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
def default_db_path
|
|
256
|
-
|
|
257
|
-
Rails.root.join("tmp", "profiler", "profiler.db")
|
|
258
|
-
else
|
|
259
|
-
File.expand_path("tmp/profiler/profiler.db", Dir.pwd)
|
|
260
|
-
end
|
|
256
|
+
File.join(Profiler.configuration.tmp_path.to_s, "profiler.db")
|
|
261
257
|
end
|
|
262
258
|
|
|
263
259
|
def default_blob_path
|
|
264
|
-
|
|
265
|
-
Rails.root.join("tmp", "profiler", "blobs")
|
|
266
|
-
else
|
|
267
|
-
File.expand_path("tmp/profiler/blobs", Dir.pwd)
|
|
268
|
-
end
|
|
260
|
+
File.join(Profiler.configuration.tmp_path.to_s, "blobs")
|
|
269
261
|
end
|
|
270
262
|
end
|
|
271
263
|
end
|
data/lib/profiler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|