rails_pulse 0.2.3 → 0.2.5.pre.pre.2
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/README.md +270 -13
- data/Rakefile +142 -8
- data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
- data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
- data/app/controllers/concerns/chart_table_concern.rb +3 -3
- data/app/controllers/rails_pulse/application_controller.rb +20 -3
- data/app/controllers/rails_pulse/assets_controller.rb +18 -2
- data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
- data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
- data/app/controllers/rails_pulse/operations_controller.rb +43 -31
- data/app/controllers/rails_pulse/queries_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +3 -8
- data/app/controllers/rails_pulse/routes_controller.rb +1 -1
- data/app/controllers/rails_pulse/tags_controller.rb +31 -5
- data/app/helpers/rails_pulse/application_helper.rb +79 -3
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
- data/app/helpers/rails_pulse/chart_helper.rb +32 -2
- data/app/helpers/rails_pulse/status_helper.rb +16 -0
- data/app/helpers/rails_pulse/tags_helper.rb +39 -1
- data/app/javascript/rails_pulse/application.js +3 -54
- data/app/javascript/rails_pulse/controllers/chart_controller.js +333 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +9 -14
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +27 -33
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +0 -2
- data/app/jobs/rails_pulse/cleanup_job.rb +0 -2
- data/app/jobs/rails_pulse/summary_job.rb +0 -2
- data/app/models/concerns/rails_pulse/taggable.rb +25 -2
- data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/job.rb +85 -0
- data/app/models/rails_pulse/job_run.rb +76 -0
- data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
- data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
- data/app/models/rails_pulse/operation.rb +16 -3
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/tables/index.rb +2 -1
- data/app/models/rails_pulse/query.rb +10 -1
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +1 -1
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/tables/index.rb +2 -1
- data/app/models/rails_pulse/summary.rb +10 -3
- data/app/services/rails_pulse/summary_service.rb +46 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/application.html.erb +23 -0
- data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/components/_table.html.erb +7 -4
- data/app/views/rails_pulse/components/_table_pagination.html.erb +8 -6
- data/app/views/rails_pulse/csp_test/show.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +5 -4
- data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
- data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
- data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
- data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
- data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
- data/app/views/rails_pulse/jobs/index.html.erb +34 -0
- data/app/views/rails_pulse/jobs/show.html.erb +49 -0
- data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
- data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
- data/app/views/rails_pulse/operations/show.html.erb +10 -8
- data/app/views/rails_pulse/queries/_table.html.erb +3 -3
- data/app/views/rails_pulse/queries/index.html.erb +2 -1
- data/app/views/rails_pulse/queries/show.html.erb +2 -1
- data/app/views/rails_pulse/requests/_table.html.erb +6 -6
- data/app/views/rails_pulse/routes/_table.html.erb +3 -3
- data/app/views/rails_pulse/routes/index.html.erb +2 -1
- data/app/views/rails_pulse/routes/show.html.erb +3 -2
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
- data/config/brakeman.ignore +213 -0
- data/config/brakeman.yml +68 -0
- data/config/importmap.rb +1 -1
- data/config/initializers/rails_pulse.rb +52 -0
- data/config/routes.rb +6 -0
- data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
- data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
- data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
- data/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
- data/lib/rails_pulse/active_job_extensions.rb +13 -0
- data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
- data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
- data/lib/rails_pulse/cleanup_service.rb +65 -0
- data/lib/rails_pulse/configuration.rb +80 -7
- data/lib/rails_pulse/engine.rb +29 -28
- data/lib/rails_pulse/extensions/active_record.rb +82 -0
- data/lib/rails_pulse/job_run_collector.rb +172 -0
- data/lib/rails_pulse/middleware/request_collector.rb +20 -43
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
- data/lib/rails_pulse/tracker.rb +82 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +2 -0
- data/lib/rails_pulse_server.ru +107 -0
- data/lib/tasks/rails_pulse_benchmark.rake +382 -0
- data/public/rails-pulse-assets/csp-test.js +10 -10
- data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +48 -48
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +38 -30
- data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
- data/config/initializers/rails_charts_csp_patch.rb +0 -75
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignored_warnings": [
|
|
3
|
+
{
|
|
4
|
+
"warning_type": "SQL Injection",
|
|
5
|
+
"warning_code": 0,
|
|
6
|
+
"fingerprint": "3d4cff0f317df7445891a1fc467912f3ad09e869da9b3e8433d5897354f5d849",
|
|
7
|
+
"check_name": "SQL",
|
|
8
|
+
"message": "Possible SQL injection",
|
|
9
|
+
"file": "app/models/rails_pulse/request.rb",
|
|
10
|
+
"line": 40,
|
|
11
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
12
|
+
"code": "Arel.sql(\"FLOOR(#{parent.table[:status].name} / 100)\")",
|
|
13
|
+
"render_path": null,
|
|
14
|
+
"location": {
|
|
15
|
+
"type": "method",
|
|
16
|
+
"class": "Request",
|
|
17
|
+
"method": null
|
|
18
|
+
},
|
|
19
|
+
"user_input": "parent.table[:status].name",
|
|
20
|
+
"confidence": "Medium",
|
|
21
|
+
"cwe_id": [
|
|
22
|
+
89
|
|
23
|
+
],
|
|
24
|
+
"note": "Reviewed: Using Arel.sql with column name from ActiveRecord table definition - safe, not user input"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"warning_type": "SQL Injection",
|
|
28
|
+
"warning_code": 0,
|
|
29
|
+
"fingerprint": "dc13170579f824326ffcaf309858f90fc807c7bdc4bfe41df3bb5d2b8f719a12",
|
|
30
|
+
"check_name": "SQL",
|
|
31
|
+
"message": "Possible SQL injection",
|
|
32
|
+
"file": "app/models/rails_pulse/request.rb",
|
|
33
|
+
"line": 54,
|
|
34
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
35
|
+
"code": "Arel.sql with configuration thresholds",
|
|
36
|
+
"render_path": null,
|
|
37
|
+
"location": {
|
|
38
|
+
"type": "method",
|
|
39
|
+
"class": "Request",
|
|
40
|
+
"method": null
|
|
41
|
+
},
|
|
42
|
+
"user_input": "((RailsPulse.configuration rescue nil).request_thresholds or { :slow => 500, :very_slow => 1000, :critical => 2000 })[:slow]",
|
|
43
|
+
"confidence": "Medium",
|
|
44
|
+
"cwe_id": [
|
|
45
|
+
89
|
|
46
|
+
],
|
|
47
|
+
"note": "Reviewed: Using Arel.sql with configuration values from RailsPulse.configuration - safe, not user input"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"warning_type": "SQL Injection",
|
|
51
|
+
"warning_code": 0,
|
|
52
|
+
"fingerprint": "f3a63d26866552b7e487f0de0f73af1c6575a62a0bbea859a52fd24912b06f3d",
|
|
53
|
+
"check_name": "SQL",
|
|
54
|
+
"message": "Possible SQL injection",
|
|
55
|
+
"file": "app/models/rails_pulse/jobs/cards/average_duration.rb",
|
|
56
|
+
"line": 21,
|
|
57
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
58
|
+
"code": "quote(current_window_start)",
|
|
59
|
+
"render_path": null,
|
|
60
|
+
"location": {
|
|
61
|
+
"type": "method",
|
|
62
|
+
"class": "RailsPulse::Jobs::Cards::AverageDuration",
|
|
63
|
+
"method": "to_metric_card"
|
|
64
|
+
},
|
|
65
|
+
"user_input": "quote(current_window_start)",
|
|
66
|
+
"confidence": "Medium",
|
|
67
|
+
"cwe_id": [
|
|
68
|
+
89
|
|
69
|
+
],
|
|
70
|
+
"note": "Reviewed: Uses connection.quote() method with calculated timestamp - safe, properly escaped by ActiveRecord"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"warning_type": "SQL Injection",
|
|
74
|
+
"warning_code": 0,
|
|
75
|
+
"fingerprint": "e9c51f9fff0ace6daba404a62decb15efdbc72129cdf0818ee802dcfb3419c11",
|
|
76
|
+
"check_name": "SQL",
|
|
77
|
+
"message": "Possible SQL injection",
|
|
78
|
+
"file": "app/models/rails_pulse/jobs/cards/failure_rate.rb",
|
|
79
|
+
"line": 21,
|
|
80
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
81
|
+
"code": "quote(current_window_start)",
|
|
82
|
+
"render_path": null,
|
|
83
|
+
"location": {
|
|
84
|
+
"type": "method",
|
|
85
|
+
"class": "RailsPulse::Jobs::Cards::FailureRate",
|
|
86
|
+
"method": "to_metric_card"
|
|
87
|
+
},
|
|
88
|
+
"user_input": "quote(current_window_start)",
|
|
89
|
+
"confidence": "Medium",
|
|
90
|
+
"cwe_id": [
|
|
91
|
+
89
|
|
92
|
+
],
|
|
93
|
+
"note": "Reviewed: Uses connection.quote() method with calculated timestamp - safe, properly escaped by ActiveRecord"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"warning_type": "SQL Injection",
|
|
97
|
+
"warning_code": 0,
|
|
98
|
+
"fingerprint": "808fdc102a0d41199ed96c40058b819ea5a28dc4a8eeabca5c444d528006cb40",
|
|
99
|
+
"check_name": "SQL",
|
|
100
|
+
"message": "Possible SQL injection",
|
|
101
|
+
"file": "app/models/rails_pulse/jobs/cards/total_jobs.rb",
|
|
102
|
+
"line": 22,
|
|
103
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
104
|
+
"code": "quote(current_window_start)",
|
|
105
|
+
"render_path": null,
|
|
106
|
+
"location": {
|
|
107
|
+
"type": "method",
|
|
108
|
+
"class": "RailsPulse::Jobs::Cards::TotalJobs",
|
|
109
|
+
"method": "to_metric_card"
|
|
110
|
+
},
|
|
111
|
+
"user_input": "quote(current_window_start)",
|
|
112
|
+
"confidence": "Medium",
|
|
113
|
+
"cwe_id": [
|
|
114
|
+
89
|
|
115
|
+
],
|
|
116
|
+
"note": "Reviewed: Uses connection.quote() method with calculated timestamp - safe, properly escaped by ActiveRecord"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"warning_type": "SQL Injection",
|
|
120
|
+
"warning_code": 0,
|
|
121
|
+
"fingerprint": "f92a184f3bc0b0374b67cf49510f2c7dd5fb010fe86a9ae210daf920b9523214",
|
|
122
|
+
"check_name": "SQL",
|
|
123
|
+
"message": "Possible SQL injection",
|
|
124
|
+
"file": "app/models/rails_pulse/jobs/cards/total_runs.rb",
|
|
125
|
+
"line": 20,
|
|
126
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
127
|
+
"code": "quote(current_window_start)",
|
|
128
|
+
"render_path": null,
|
|
129
|
+
"location": {
|
|
130
|
+
"type": "method",
|
|
131
|
+
"class": "RailsPulse::Jobs::Cards::TotalRuns",
|
|
132
|
+
"method": "to_metric_card"
|
|
133
|
+
},
|
|
134
|
+
"user_input": "quote(current_window_start)",
|
|
135
|
+
"confidence": "Medium",
|
|
136
|
+
"cwe_id": [
|
|
137
|
+
89
|
|
138
|
+
],
|
|
139
|
+
"note": "Reviewed: Uses connection.quote() method with calculated timestamp - safe, properly escaped by ActiveRecord"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"warning_type": "SQL Injection",
|
|
143
|
+
"warning_code": 0,
|
|
144
|
+
"fingerprint": "3c816cd9f565d6592595f5566cbd9d9a19ecb51ad704477f01a9bf2b4c1f29fb",
|
|
145
|
+
"check_name": "SQL",
|
|
146
|
+
"message": "Possible SQL injection",
|
|
147
|
+
"file": "app/services/rails_pulse/analysis/explain_plan_analyzer.rb",
|
|
148
|
+
"line": 191,
|
|
149
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
150
|
+
"code": "RailsPulse::ApplicationRecord.connection.execute(\"EXPLAIN (ANALYZE, BUFFERS) #{sql}\")",
|
|
151
|
+
"render_path": null,
|
|
152
|
+
"location": {
|
|
153
|
+
"type": "method",
|
|
154
|
+
"class": "RailsPulse::Analysis::ExplainPlanAnalyzer",
|
|
155
|
+
"method": "execute_postgres_explain"
|
|
156
|
+
},
|
|
157
|
+
"user_input": "sql",
|
|
158
|
+
"confidence": "Medium",
|
|
159
|
+
"cwe_id": [
|
|
160
|
+
89
|
|
161
|
+
],
|
|
162
|
+
"note": "Reviewed: SQL comes from Rails' ActiveRecord instrumentation (payload[:sql]), not user input. This is internally-generated SQL that Rails has already executed safely. EXPLAIN is read-only."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"warning_type": "SQL Injection",
|
|
166
|
+
"warning_code": 0,
|
|
167
|
+
"fingerprint": "a57a1c949d21de15d6b48e52bfc3077e3177a13ba1d703ddf48e241fb4e81c6e",
|
|
168
|
+
"check_name": "SQL",
|
|
169
|
+
"message": "Possible SQL injection",
|
|
170
|
+
"file": "app/services/rails_pulse/analysis/explain_plan_analyzer.rb",
|
|
171
|
+
"line": 196,
|
|
172
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
173
|
+
"code": "RailsPulse::ApplicationRecord.connection.execute(\"EXPLAIN #{sql}\")",
|
|
174
|
+
"render_path": null,
|
|
175
|
+
"location": {
|
|
176
|
+
"type": "method",
|
|
177
|
+
"class": "RailsPulse::Analysis::ExplainPlanAnalyzer",
|
|
178
|
+
"method": "execute_mysql_explain"
|
|
179
|
+
},
|
|
180
|
+
"user_input": "sql",
|
|
181
|
+
"confidence": "Medium",
|
|
182
|
+
"cwe_id": [
|
|
183
|
+
89
|
|
184
|
+
],
|
|
185
|
+
"note": "Reviewed: SQL comes from Rails' ActiveRecord instrumentation (payload[:sql]), not user input. This is internally-generated SQL that Rails has already executed safely. EXPLAIN is read-only."
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"warning_type": "SQL Injection",
|
|
189
|
+
"warning_code": 0,
|
|
190
|
+
"fingerprint": "85c09ca9672d132fb206d0371e20087deaf381d89733f73d647129878b89f374",
|
|
191
|
+
"check_name": "SQL",
|
|
192
|
+
"message": "Possible SQL injection",
|
|
193
|
+
"file": "app/services/rails_pulse/analysis/explain_plan_analyzer.rb",
|
|
194
|
+
"line": 201,
|
|
195
|
+
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
196
|
+
"code": "RailsPulse::ApplicationRecord.connection.execute(\"EXPLAIN QUERY PLAN #{sql}\")",
|
|
197
|
+
"render_path": null,
|
|
198
|
+
"location": {
|
|
199
|
+
"type": "method",
|
|
200
|
+
"class": "RailsPulse::Analysis::ExplainPlanAnalyzer",
|
|
201
|
+
"method": "execute_sqlite_explain"
|
|
202
|
+
},
|
|
203
|
+
"user_input": "sql",
|
|
204
|
+
"confidence": "Medium",
|
|
205
|
+
"cwe_id": [
|
|
206
|
+
89
|
|
207
|
+
],
|
|
208
|
+
"note": "Reviewed: SQL comes from Rails' ActiveRecord instrumentation (payload[:sql]), not user input. This is internally-generated SQL that Rails has already executed safely. EXPLAIN is read-only."
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
"updated": "2025-11-30 15:10:00 +0700",
|
|
212
|
+
"brakeman_version": "7.1.1"
|
|
213
|
+
}
|
data/config/brakeman.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Brakeman configuration file for Rails Pulse Engine
|
|
3
|
+
# https://brakemanscanner.org/docs/options/
|
|
4
|
+
|
|
5
|
+
# Application path - scan the engine files
|
|
6
|
+
:app_path: "."
|
|
7
|
+
|
|
8
|
+
# This is a Rails engine, not a standalone app
|
|
9
|
+
:engine_paths:
|
|
10
|
+
- app
|
|
11
|
+
- lib
|
|
12
|
+
|
|
13
|
+
# Rails version - let Brakeman detect automatically from gemspec
|
|
14
|
+
:rails7: true
|
|
15
|
+
|
|
16
|
+
# Output format for reports (can be: text, json, html, csv, tabs, markdown)
|
|
17
|
+
:output_format: :text
|
|
18
|
+
|
|
19
|
+
# Paths to skip - don't scan dummy app or test files
|
|
20
|
+
:skip_files:
|
|
21
|
+
- test/dummy/**/*
|
|
22
|
+
- node_modules/**/*
|
|
23
|
+
- vendor/**/*
|
|
24
|
+
- gemfiles/**/*
|
|
25
|
+
|
|
26
|
+
# Paths to ignore - additional paths to skip
|
|
27
|
+
:ignore_paths:
|
|
28
|
+
- test/**/*
|
|
29
|
+
- tmp/**/*
|
|
30
|
+
- log/**/*
|
|
31
|
+
- vendor/**/*
|
|
32
|
+
- node_modules/**/*
|
|
33
|
+
- public/**/*
|
|
34
|
+
- gemfiles/**/*
|
|
35
|
+
|
|
36
|
+
# Confidence levels: 0 = high only, 1 = high and medium, 2 = all
|
|
37
|
+
:min_confidence: 1 # Report high and medium confidence warnings only
|
|
38
|
+
|
|
39
|
+
# Exit code configuration
|
|
40
|
+
:exit_on_warn: true # Exit with error code if warnings found
|
|
41
|
+
:exit_on_error: true # Exit with error code if errors found
|
|
42
|
+
|
|
43
|
+
# Interactive mode
|
|
44
|
+
:interactive: false
|
|
45
|
+
|
|
46
|
+
# Parallelization
|
|
47
|
+
:parallel_checks: true
|
|
48
|
+
|
|
49
|
+
# Report additional information
|
|
50
|
+
:report_progress: true
|
|
51
|
+
:highlight_user_input: true
|
|
52
|
+
:message_limit: 100 # Maximum number of warnings to report per type
|
|
53
|
+
|
|
54
|
+
# Ignore file for specific false positives
|
|
55
|
+
:ignore_file: config/brakeman.ignore
|
|
56
|
+
|
|
57
|
+
# Additional options
|
|
58
|
+
:absolute_paths: false # Use relative paths in reports
|
|
59
|
+
:summary_only: false # Show full details, not just summary
|
|
60
|
+
|
|
61
|
+
# Engine-specific: Skip checks that don't apply to mountable engines
|
|
62
|
+
# These checks are more relevant for standalone apps
|
|
63
|
+
:skip_checks:
|
|
64
|
+
# Skip checks that are handled by the host application
|
|
65
|
+
- CheckForgerySetting # CSRF protection is handled by host app
|
|
66
|
+
|
|
67
|
+
# Note: We're NOT skipping SQL injection checks - those should be reviewed
|
|
68
|
+
# Note: We're NOT skipping file access checks - those should be reviewed
|
data/config/importmap.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
pin "application", to: "rails_pulse/application.js"
|
|
2
2
|
|
|
3
|
-
# echarts is
|
|
3
|
+
# echarts is used for chart rendering
|
|
4
4
|
pin "echarts", to: "echarts.min.js"
|
|
5
5
|
# pin "echarts/theme/inspired", to: "echarts/theme/inspired.js"
|
|
6
6
|
pin "rails_pulse/theme", to: "rails_pulse/theme.js"
|
|
@@ -71,6 +71,58 @@ RailsPulse.configure do |config|
|
|
|
71
71
|
config.ignored_requests = []
|
|
72
72
|
config.ignored_queries = []
|
|
73
73
|
|
|
74
|
+
# ====================================================================================================
|
|
75
|
+
# TAGGING
|
|
76
|
+
# ====================================================================================================
|
|
77
|
+
# Define custom tags for categorizing routes, requests, and queries.
|
|
78
|
+
# You can add any custom tags you want for filtering and organization.
|
|
79
|
+
#
|
|
80
|
+
# Tag names should be in present tense and describe the current state or category.
|
|
81
|
+
# Examples of good tag names:
|
|
82
|
+
# - "critical" (for high-priority endpoints)
|
|
83
|
+
# - "experimental" (for routes under development)
|
|
84
|
+
# - "deprecated" (for routes being phased out)
|
|
85
|
+
# - "external" (for third-party API calls)
|
|
86
|
+
# - "background" (for async job-related operations)
|
|
87
|
+
# - "admin" (for administrative routes)
|
|
88
|
+
# - "public" (for public-facing routes)
|
|
89
|
+
#
|
|
90
|
+
# Example configuration:
|
|
91
|
+
# config.tags = ["ignored", "critical", "experimental", "deprecated", "external", "admin"]
|
|
92
|
+
|
|
93
|
+
config.tags = [ "ignored", "critical", "experimental" ]
|
|
94
|
+
|
|
95
|
+
# ====================================================================================================
|
|
96
|
+
# BACKGROUND JOBS
|
|
97
|
+
# ====================================================================================================
|
|
98
|
+
# Configure background job monitoring and tracking.
|
|
99
|
+
# When enabled, Rails Pulse will track job executions, durations, failures, and retries.
|
|
100
|
+
# Supports ActiveJob, Sidekiq, and Delayed Job.
|
|
101
|
+
|
|
102
|
+
# Enable or disable background job tracking
|
|
103
|
+
config.track_jobs = false
|
|
104
|
+
|
|
105
|
+
# Thresholds for job execution times (in milliseconds)
|
|
106
|
+
config.job_thresholds = {
|
|
107
|
+
slow: 5_000, # 5 seconds
|
|
108
|
+
very_slow: 30_000, # 30 seconds
|
|
109
|
+
critical: 60_000 # 1 minute
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Job classes to ignore from tracking (by class name)
|
|
113
|
+
# Examples:
|
|
114
|
+
# config.ignored_jobs = ["ActionMailer::MailDeliveryJob", "MyApp::HealthCheckJob"]
|
|
115
|
+
config.ignored_jobs = []
|
|
116
|
+
|
|
117
|
+
# Queue names to ignore from tracking
|
|
118
|
+
# Examples:
|
|
119
|
+
# config.ignored_queues = ["low_priority", "mailers"]
|
|
120
|
+
config.ignored_queues = []
|
|
121
|
+
|
|
122
|
+
# Capture job arguments for debugging (may contain sensitive data)
|
|
123
|
+
# Set to false in production to avoid storing potentially sensitive information
|
|
124
|
+
config.capture_job_arguments = true
|
|
125
|
+
|
|
74
126
|
# ====================================================================================================
|
|
75
127
|
# DATABASE CONFIGURATION
|
|
76
128
|
# ====================================================================================================
|
data/config/routes.rb
CHANGED
|
@@ -10,6 +10,12 @@ RailsPulse::Engine.routes.draw do
|
|
|
10
10
|
end
|
|
11
11
|
resources :operations, only: %i[show]
|
|
12
12
|
resources :caches, only: %i[show], as: :cache
|
|
13
|
+
|
|
14
|
+
if RailsPulse.configuration.track_jobs
|
|
15
|
+
resources :jobs, only: %i[index show], param: :id do
|
|
16
|
+
resources :runs, only: %i[index show], controller: "job_runs"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
13
19
|
patch "pagination/limit", to: "application#set_pagination_limit"
|
|
14
20
|
patch "settings/global_filters", to: "application#set_global_filters"
|
|
15
21
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Add background job tracking to Rails Pulse
|
|
2
|
+
class AddJobsToRailsPulse < ActiveRecord::Migration[7.0]
|
|
3
|
+
def up
|
|
4
|
+
# Create jobs table for storing job definitions
|
|
5
|
+
unless table_exists?(:rails_pulse_jobs)
|
|
6
|
+
create_table :rails_pulse_jobs do |t|
|
|
7
|
+
t.string :name, null: false, comment: "Job class name"
|
|
8
|
+
t.string :queue_name, comment: "Default queue"
|
|
9
|
+
t.text :description, comment: "Optional description"
|
|
10
|
+
t.integer :runs_count, null: false, default: 0, comment: "Cache of total runs"
|
|
11
|
+
t.integer :failures_count, null: false, default: 0, comment: "Cache of failed runs"
|
|
12
|
+
t.integer :retries_count, null: false, default: 0, comment: "Cache of retried runs"
|
|
13
|
+
t.decimal :avg_duration, precision: 15, scale: 6, comment: "Average duration in milliseconds"
|
|
14
|
+
t.text :tags, comment: "JSON array of tags"
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :rails_pulse_jobs, :name, unique: true, name: "index_rails_pulse_jobs_on_name"
|
|
19
|
+
add_index :rails_pulse_jobs, :queue_name, name: "index_rails_pulse_jobs_on_queue"
|
|
20
|
+
add_index :rails_pulse_jobs, :runs_count, name: "index_rails_pulse_jobs_on_runs_count"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create job_runs table for individual job executions
|
|
24
|
+
unless table_exists?(:rails_pulse_job_runs)
|
|
25
|
+
create_table :rails_pulse_job_runs do |t|
|
|
26
|
+
t.references :job, null: false, foreign_key: { to_table: :rails_pulse_jobs }, comment: "Link to job definition"
|
|
27
|
+
t.string :run_id, null: false, comment: "Adapter specific run id"
|
|
28
|
+
t.decimal :duration, precision: 15, scale: 6, comment: "Execution duration in milliseconds"
|
|
29
|
+
t.string :status, null: false, comment: "Execution status"
|
|
30
|
+
t.string :error_class, comment: "Error class name"
|
|
31
|
+
t.text :error_message, comment: "Error message"
|
|
32
|
+
t.integer :attempts, null: false, default: 0, comment: "Retry attempts"
|
|
33
|
+
t.timestamp :occurred_at, null: false, comment: "When the job started"
|
|
34
|
+
t.timestamp :enqueued_at, comment: "When the job was enqueued"
|
|
35
|
+
t.text :arguments, comment: "Serialized arguments"
|
|
36
|
+
t.string :adapter, comment: "Queue adapter"
|
|
37
|
+
t.text :tags, comment: "Execution tags"
|
|
38
|
+
t.timestamps
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
add_index :rails_pulse_job_runs, :run_id, unique: true, name: "index_rails_pulse_job_runs_on_run_id"
|
|
42
|
+
add_index :rails_pulse_job_runs, [ :job_id, :occurred_at ], name: "index_rails_pulse_job_runs_on_job_and_occurred"
|
|
43
|
+
add_index :rails_pulse_job_runs, :occurred_at, name: "index_rails_pulse_job_runs_on_occurred_at"
|
|
44
|
+
add_index :rails_pulse_job_runs, :status, name: "index_rails_pulse_job_runs_on_status"
|
|
45
|
+
add_index :rails_pulse_job_runs, [ :job_id, :status ], name: "index_rails_pulse_job_runs_on_job_and_status"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add job_run_id to operations table if it doesn't exist
|
|
49
|
+
if table_exists?(:rails_pulse_operations) && !column_exists?(:rails_pulse_operations, :job_run_id)
|
|
50
|
+
# Make request_id nullable to allow job operations
|
|
51
|
+
change_column_null :rails_pulse_operations, :request_id, true
|
|
52
|
+
|
|
53
|
+
# Add job_run_id reference
|
|
54
|
+
add_reference :rails_pulse_operations, :job_run,
|
|
55
|
+
null: true,
|
|
56
|
+
foreign_key: { to_table: :rails_pulse_job_runs },
|
|
57
|
+
comment: "Link to a background job execution"
|
|
58
|
+
|
|
59
|
+
# Add check constraint for PostgreSQL and MySQL to ensure either request_id or job_run_id is present
|
|
60
|
+
adapter = connection.adapter_name.downcase
|
|
61
|
+
if adapter.include?("postgres") || adapter.include?("mysql")
|
|
62
|
+
execute <<-SQL
|
|
63
|
+
ALTER TABLE rails_pulse_operations
|
|
64
|
+
ADD CONSTRAINT rails_pulse_operations_request_or_job_run
|
|
65
|
+
CHECK (request_id IS NOT NULL OR job_run_id IS NOT NULL)
|
|
66
|
+
SQL
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def down
|
|
72
|
+
# Remove check constraint first
|
|
73
|
+
adapter = connection.adapter_name.downcase
|
|
74
|
+
if adapter.include?("postgres") || adapter.include?("mysql")
|
|
75
|
+
execute <<-SQL
|
|
76
|
+
ALTER TABLE rails_pulse_operations
|
|
77
|
+
DROP CONSTRAINT IF EXISTS rails_pulse_operations_request_or_job_run
|
|
78
|
+
SQL
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Remove job_run_id from operations
|
|
82
|
+
if column_exists?(:rails_pulse_operations, :job_run_id)
|
|
83
|
+
remove_reference :rails_pulse_operations, :job_run, foreign_key: { to_table: :rails_pulse_job_runs }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Make request_id non-nullable again
|
|
87
|
+
if column_exists?(:rails_pulse_operations, :request_id)
|
|
88
|
+
change_column_null :rails_pulse_operations, :request_id, false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Drop job tables
|
|
92
|
+
drop_table :rails_pulse_job_runs if table_exists?(:rails_pulse_job_runs)
|
|
93
|
+
drop_table :rails_pulse_jobs if table_exists?(:rails_pulse_jobs)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add query fingerprinting to handle long SQL queries
|
|
4
|
+
# Uses MD5 hash of normalized SQL as unique identifier
|
|
5
|
+
class AddQueryFingerprinting < ActiveRecord::Migration[7.0]
|
|
6
|
+
def up
|
|
7
|
+
return unless table_exists?(:rails_pulse_queries)
|
|
8
|
+
|
|
9
|
+
# Add hashed_sql column if it doesn't exist
|
|
10
|
+
unless column_exists?(:rails_pulse_queries, :hashed_sql)
|
|
11
|
+
say "Adding hashed_sql column to rails_pulse_queries..."
|
|
12
|
+
add_column :rails_pulse_queries, :hashed_sql, :string, limit: 32
|
|
13
|
+
|
|
14
|
+
# Backfill existing records with MD5 hash
|
|
15
|
+
say "Backfilling query hashes for existing records..."
|
|
16
|
+
backfill_query_hashes
|
|
17
|
+
|
|
18
|
+
# Make it required and unique
|
|
19
|
+
say "Adding constraints and indexes..."
|
|
20
|
+
change_column_null :rails_pulse_queries, :hashed_sql, false
|
|
21
|
+
add_index :rails_pulse_queries, :hashed_sql, unique: true,
|
|
22
|
+
name: "index_rails_pulse_queries_on_hashed_sql"
|
|
23
|
+
|
|
24
|
+
# Remove old index
|
|
25
|
+
say "Removing old normalized_sql index..."
|
|
26
|
+
if index_exists?(:rails_pulse_queries, :normalized_sql, name: "index_rails_pulse_queries_on_normalized_sql")
|
|
27
|
+
remove_index :rails_pulse_queries, :normalized_sql, name: "index_rails_pulse_queries_on_normalized_sql"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Change normalized_sql to text (remove 1000 char limit)
|
|
31
|
+
say "Changing normalized_sql to text type (removing length limit)..."
|
|
32
|
+
change_column :rails_pulse_queries, :normalized_sql, :text
|
|
33
|
+
|
|
34
|
+
say "Query fingerprinting migration completed successfully!", :green
|
|
35
|
+
else
|
|
36
|
+
say "Query fingerprinting already applied. Skipping.", :yellow
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def down
|
|
41
|
+
# Prevent rollback if there are queries longer than 1000 characters
|
|
42
|
+
if has_long_queries?
|
|
43
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
44
|
+
"Cannot rollback: normalized_sql contains queries longer than 1000 characters. " \
|
|
45
|
+
"Rolling back would truncate data."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return unless column_exists?(:rails_pulse_queries, :hashed_sql)
|
|
49
|
+
|
|
50
|
+
say "Rolling back query fingerprinting changes..."
|
|
51
|
+
|
|
52
|
+
# Restore varchar limit (safe because we checked for long queries)
|
|
53
|
+
change_column :rails_pulse_queries, :normalized_sql, :string, limit: 1000
|
|
54
|
+
|
|
55
|
+
# Restore old index
|
|
56
|
+
add_index :rails_pulse_queries, :normalized_sql, unique: true,
|
|
57
|
+
name: "index_rails_pulse_queries_on_normalized_sql", length: 191
|
|
58
|
+
|
|
59
|
+
# Remove new index
|
|
60
|
+
if index_exists?(:rails_pulse_queries, :hashed_sql, name: "index_rails_pulse_queries_on_hashed_sql")
|
|
61
|
+
remove_index :rails_pulse_queries, :hashed_sql, name: "index_rails_pulse_queries_on_hashed_sql"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Remove hashed_sql column
|
|
65
|
+
remove_column :rails_pulse_queries, :hashed_sql
|
|
66
|
+
|
|
67
|
+
say "Rollback completed.", :green
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def backfill_query_hashes
|
|
73
|
+
adapter = connection.adapter_name.downcase
|
|
74
|
+
|
|
75
|
+
if adapter.include?("postgres") || adapter.include?("mysql")
|
|
76
|
+
# Use database MD5 function for better performance
|
|
77
|
+
execute <<-SQL
|
|
78
|
+
UPDATE rails_pulse_queries
|
|
79
|
+
SET hashed_sql = MD5(normalized_sql)
|
|
80
|
+
WHERE hashed_sql IS NULL
|
|
81
|
+
SQL
|
|
82
|
+
else
|
|
83
|
+
# SQLite - use Ruby MD5 (slower but works)
|
|
84
|
+
require "digest"
|
|
85
|
+
RailsPulse::Query.where(hashed_sql: nil).find_each do |query|
|
|
86
|
+
query.update_column(:hashed_sql, Digest::MD5.hexdigest(query.normalized_sql))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Handle potential duplicates (queries with same normalized SQL)
|
|
91
|
+
handle_duplicate_hashes
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_duplicate_hashes
|
|
95
|
+
# Group queries by hash and find duplicates
|
|
96
|
+
query_groups = RailsPulse::Query
|
|
97
|
+
.select(:hashed_sql)
|
|
98
|
+
.group(:hashed_sql)
|
|
99
|
+
.having("COUNT(*) > 1")
|
|
100
|
+
.pluck(:hashed_sql)
|
|
101
|
+
|
|
102
|
+
return if query_groups.empty?
|
|
103
|
+
|
|
104
|
+
say "Found #{query_groups.size} duplicate query groups. Merging...", :yellow
|
|
105
|
+
|
|
106
|
+
query_groups.each do |hash|
|
|
107
|
+
# Get all queries with this hash, ordered by creation time
|
|
108
|
+
queries = RailsPulse::Query.where(hashed_sql: hash).order(:created_at).to_a
|
|
109
|
+
keep_query = queries.first
|
|
110
|
+
duplicate_queries = queries[1..]
|
|
111
|
+
|
|
112
|
+
duplicate_queries.each do |dup_query|
|
|
113
|
+
# Count operations before merge
|
|
114
|
+
operations_count = RailsPulse::Operation.where(query_id: dup_query.id).count
|
|
115
|
+
|
|
116
|
+
if operations_count > 0
|
|
117
|
+
say " Merging #{operations_count} operations from query ##{dup_query.id} into ##{keep_query.id}"
|
|
118
|
+
|
|
119
|
+
# Reassign operations to the kept query
|
|
120
|
+
RailsPulse::Operation.where(query_id: dup_query.id).update_all(query_id: keep_query.id)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Delete the duplicate query
|
|
124
|
+
dup_query.delete
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
say "Merged #{query_groups.size} duplicate query groups successfully.", :green
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def has_long_queries?
|
|
132
|
+
# Check if any queries exceed 1000 characters
|
|
133
|
+
adapter = connection.adapter_name.downcase
|
|
134
|
+
|
|
135
|
+
if adapter.include?("postgres")
|
|
136
|
+
result = execute("SELECT EXISTS(SELECT 1 FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000)")
|
|
137
|
+
# Handle both Rails 7.2 and 8.0 result formats
|
|
138
|
+
result.first.is_a?(Hash) ? result.first["exists"] == "t" : result.first[0] == "t"
|
|
139
|
+
elsif adapter.include?("mysql")
|
|
140
|
+
result = execute("SELECT EXISTS(SELECT 1 FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000) as result")
|
|
141
|
+
# Handle both result formats
|
|
142
|
+
result.first.is_a?(Hash) ? result.first["result"] == 1 : result.first[0] == 1
|
|
143
|
+
else
|
|
144
|
+
# SQLite
|
|
145
|
+
result = execute("SELECT COUNT(*) as count FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000")
|
|
146
|
+
count = result.first.is_a?(Hash) ? result.first["count"] : result.first[0]
|
|
147
|
+
count.to_i > 0
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Add index to rails_pulse_requests.request_uuid for efficient lookups
|
|
2
|
+
class AddIndexToRequestUuid < ActiveRecord::Migration[7.0]
|
|
3
|
+
def up
|
|
4
|
+
unless index_exists?(:rails_pulse_requests, :request_uuid)
|
|
5
|
+
add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def down
|
|
10
|
+
if index_exists?(:rails_pulse_requests, :request_uuid)
|
|
11
|
+
remove_index :rails_pulse_requests, name: "index_rails_pulse_requests_on_request_uuid"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|