rails_pulse 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +79 -177
- data/Rakefile +77 -2
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +28 -17
- data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
- data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
- data/app/controllers/concerns/chart_table_concern.rb +21 -4
- data/app/controllers/concerns/response_range_concern.rb +6 -3
- data/app/controllers/concerns/time_range_concern.rb +5 -10
- data/app/controllers/concerns/zoom_range_concern.rb +32 -1
- data/app/controllers/rails_pulse/application_controller.rb +13 -5
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -0
- data/app/controllers/rails_pulse/queries_controller.rb +111 -51
- data/app/controllers/rails_pulse/requests_controller.rb +37 -12
- data/app/controllers/rails_pulse/routes_controller.rb +98 -24
- data/app/helpers/rails_pulse/application_helper.rb +0 -1
- data/app/helpers/rails_pulse/chart_formatters.rb +3 -3
- data/app/helpers/rails_pulse/chart_helper.rb +21 -9
- data/app/helpers/rails_pulse/status_helper.rb +10 -4
- data/app/javascript/rails_pulse/application.js +34 -3
- data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
- data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +353 -39
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +17 -27
- data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
- data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +41 -0
- data/app/jobs/rails_pulse/summary_job.rb +53 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +28 -7
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +18 -22
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +18 -7
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +34 -41
- data/app/models/rails_pulse/operation.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +49 -25
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +40 -28
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +37 -43
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +23 -97
- data/app/models/rails_pulse/queries/tables/index.rb +74 -0
- data/app/models/rails_pulse/query.rb +47 -0
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +23 -84
- data/app/models/rails_pulse/route.rb +1 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +45 -25
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +43 -45
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +36 -44
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +37 -27
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +23 -100
- data/app/models/rails_pulse/routes/tables/index.rb +57 -40
- data/app/models/rails_pulse/summary.rb +143 -0
- data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
- data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
- data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
- data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
- data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
- data/app/services/rails_pulse/query_analysis_service.rb +125 -0
- data/app/services/rails_pulse/summary_service.rb +199 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +4 -6
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
- data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
- data/app/views/rails_pulse/components/_empty_state.html.erb +11 -0
- data/app/views/rails_pulse/components/_metric_card.html.erb +37 -28
- data/app/views/rails_pulse/components/_panel.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
- data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +55 -37
- data/app/views/rails_pulse/operations/show.html.erb +17 -15
- data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
- data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +2 -2
- data/app/views/rails_pulse/queries/_table.html.erb +11 -13
- data/app/views/rails_pulse/queries/index.html.erb +32 -28
- data/app/views/rails_pulse/queries/show.html.erb +45 -34
- data/app/views/rails_pulse/requests/_operations.html.erb +38 -45
- data/app/views/rails_pulse/requests/_table.html.erb +3 -3
- data/app/views/rails_pulse/requests/index.html.erb +33 -28
- data/app/views/rails_pulse/routes/_table.html.erb +14 -14
- data/app/views/rails_pulse/routes/index.html.erb +34 -29
- data/app/views/rails_pulse/routes/show.html.erb +43 -36
- data/config/initializers/rails_pulse.rb +0 -12
- data/config/routes.rb +5 -1
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +54 -0
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
- data/db/rails_pulse_schema.rb +130 -0
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
- data/lib/generators/rails_pulse/install_generator.rb +94 -4
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +60 -0
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +0 -12
- data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
- data/lib/rails_pulse/configuration.rb +0 -11
- data/lib/rails_pulse/engine.rb +0 -1
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +77 -0
- 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 +53 -53
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- data/public/rails-pulse-assets/search.svg +43 -0
- metadata +48 -14
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/controllers/rails_pulse/caches_controller.rb +0 -115
- data/app/helpers/rails_pulse/cached_component_helper.rb +0 -73
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
- data/app/models/rails_pulse/component_cache_key.rb +0 -33
- data/app/views/rails_pulse/caches/show.html.erb +0 -9
- data/db/migrate/20250227235904_create_routes.rb +0 -12
- data/db/migrate/20250227235915_create_requests.rb +0 -19
- data/db/migrate/20250228000000_create_queries.rb +0 -14
- data/db/migrate/20250228000056_create_operations.rb +0 -24
- data/lib/rails_pulse/migration.rb +0 -29
@@ -56,3 +56,16 @@
|
|
56
56
|
--badge-border-color: transparent;
|
57
57
|
--badge-color: var(--color-negative);
|
58
58
|
}
|
59
|
+
|
60
|
+
/* Trend badge icon lightening (dark mode only) */
|
61
|
+
.badge--trend rails-pulse-icon { color: var(--badge-color, currentColor); }
|
62
|
+
html[data-color-scheme="dark"] .badge--trend rails-pulse-icon {
|
63
|
+
/* Lighten icon relative to badge text color for contrast */
|
64
|
+
color: color-mix(in srgb, var(--badge-color) 55%, white 45%);
|
65
|
+
}
|
66
|
+
|
67
|
+
/* Trend amount lightening (dark mode only) */
|
68
|
+
.badge--trend .badge__trend-amount { color: var(--badge-color, currentColor); }
|
69
|
+
html[data-color-scheme="dark"] .badge--trend .badge__trend-amount {
|
70
|
+
color: color-mix(in srgb, var(--badge-color) 55%, white 45%);
|
71
|
+
}
|
@@ -5,6 +5,10 @@
|
|
5
5
|
--color-text-reversed: white;
|
6
6
|
--color-text-subtle: var(--zinc-500);
|
7
7
|
--color-link: var(--blue-700);
|
8
|
+
/* Header tokens */
|
9
|
+
--header-bg: #ffc91f;
|
10
|
+
--header-link: black;
|
11
|
+
--header-link-hover-bg: #ffe284;
|
8
12
|
--color-border-light: var(--zinc-100);
|
9
13
|
--color-border: var(--zinc-200);
|
10
14
|
--color-border-dark: var(--zinc-400);
|
@@ -30,8 +34,9 @@ html[data-color-scheme="dark"] {
|
|
30
34
|
--color-bg: var(--zinc-800);
|
31
35
|
--color-text: white;
|
32
36
|
--color-text-reversed: black;
|
33
|
-
--color-text-subtle: var(--zinc-
|
34
|
-
|
37
|
+
--color-text-subtle: var(--zinc-300);
|
38
|
+
/* Use brand yellow for links in dark mode */
|
39
|
+
--color-link: #ffc91f;
|
35
40
|
--color-border-light: var(--zinc-900);
|
36
41
|
--color-border: var(--zinc-800);
|
37
42
|
--color-border-dark: var(--zinc-600);
|
@@ -39,6 +44,11 @@ html[data-color-scheme="dark"] {
|
|
39
44
|
--color-selected-dark: var(--blue-800);
|
40
45
|
--color-highlight: var(--yellow-900);
|
41
46
|
|
47
|
+
/* Header tokens */
|
48
|
+
--header-bg: rgb(32, 32, 32);
|
49
|
+
--header-link: #ffc91f;
|
50
|
+
--header-link-hover-bg: #ffe284; /* keep existing hover color */
|
51
|
+
|
42
52
|
/* Accent colors */
|
43
53
|
--color-primary: var(--zinc-50);
|
44
54
|
--color-secondary: var(--zinc-800);
|
@@ -0,0 +1,30 @@
|
|
1
|
+
.collapsible-code.collapsed pre {
|
2
|
+
max-height: 4.5em;
|
3
|
+
overflow: hidden;
|
4
|
+
position: relative;
|
5
|
+
}
|
6
|
+
|
7
|
+
.collapsible-code.collapsed pre::after {
|
8
|
+
content: '';
|
9
|
+
position: absolute;
|
10
|
+
bottom: 0;
|
11
|
+
left: 0;
|
12
|
+
right: 0;
|
13
|
+
height: 1em;
|
14
|
+
background: linear-gradient(transparent, var(--color-border-light));
|
15
|
+
pointer-events: none;
|
16
|
+
}
|
17
|
+
|
18
|
+
.collapsible-toggle {
|
19
|
+
margin-top: 0.5rem;
|
20
|
+
font-size: 0.875rem;
|
21
|
+
color: var(--color-link);
|
22
|
+
text-decoration: underline;
|
23
|
+
transform: lowercase;
|
24
|
+
cursor: pointer;
|
25
|
+
border: none;
|
26
|
+
background: none;
|
27
|
+
padding: 0;
|
28
|
+
font-weight: normal;
|
29
|
+
margin-left: 10px;
|
30
|
+
}
|
@@ -3,22 +3,74 @@
|
|
3
3
|
justify-content: space-between;
|
4
4
|
width: 100%;
|
5
5
|
gap: var(--column-gap, 0.5rem);
|
6
|
+
align-items: stretch;
|
6
7
|
}
|
7
8
|
|
8
9
|
.row > * {
|
9
10
|
flex: 1;
|
10
11
|
min-width: 0;
|
12
|
+
display: flex;
|
13
|
+
flex-direction: column;
|
11
14
|
}
|
12
15
|
|
13
|
-
/*
|
16
|
+
/* Ensure metric cards and their panels stretch to full height */
|
17
|
+
.row > .grid-item {
|
18
|
+
display: flex;
|
19
|
+
flex-direction: column;
|
20
|
+
}
|
21
|
+
|
22
|
+
.row > .grid-item > * {
|
23
|
+
flex: 1;
|
24
|
+
}
|
25
|
+
|
26
|
+
/* Responsive layout for screens smaller than 768px */
|
14
27
|
@media (max-width: 768px) {
|
15
28
|
.row {
|
16
|
-
|
29
|
+
display: flex;
|
30
|
+
flex-wrap: wrap;
|
31
|
+
justify-content: space-between;
|
17
32
|
gap: 0.5rem;
|
33
|
+
align-items: flex-start;
|
18
34
|
}
|
19
35
|
|
20
36
|
.row > * {
|
37
|
+
flex: 0 0 calc(50% - 0.25rem);
|
38
|
+
min-width: 0;
|
39
|
+
height: auto;
|
40
|
+
}
|
41
|
+
|
42
|
+
.row > .grid-item {
|
43
|
+
height: auto;
|
44
|
+
}
|
45
|
+
|
46
|
+
.row > .grid-item > * {
|
21
47
|
flex: none;
|
22
|
-
|
48
|
+
}
|
49
|
+
|
50
|
+
/* Tables should stack in single column on mobile */
|
51
|
+
.row:has(.table-container) > * {
|
52
|
+
flex: 0 0 100%;
|
53
|
+
}
|
54
|
+
|
55
|
+
/* Single column for very small screens */
|
56
|
+
@media (max-width: 480px) {
|
57
|
+
.row > * {
|
58
|
+
flex: 0 0 100%;
|
59
|
+
}
|
60
|
+
|
61
|
+
.row > .grid-item {
|
62
|
+
min-height: auto;
|
63
|
+
}
|
64
|
+
|
65
|
+
/* Make metric cards more compact on mobile */
|
66
|
+
.row > .grid-item .card {
|
67
|
+
padding: var(--size-3);
|
68
|
+
}
|
69
|
+
|
70
|
+
/* Make charts smaller on mobile */
|
71
|
+
.row > .grid-item .chart-container {
|
72
|
+
height: 60px !important;
|
73
|
+
max-height: 60px;
|
74
|
+
}
|
23
75
|
}
|
24
76
|
}
|
@@ -77,3 +77,26 @@
|
|
77
77
|
padding: var(--size-0_5) var(--size-2);
|
78
78
|
row-gap: var(--size-1);
|
79
79
|
}
|
80
|
+
|
81
|
+
/* Sheet component styles for mobile menu */
|
82
|
+
.sheet {
|
83
|
+
border: 0;
|
84
|
+
background: var(--color-bg);
|
85
|
+
max-block-size: none;
|
86
|
+
max-inline-size: none;
|
87
|
+
padding: 0;
|
88
|
+
}
|
89
|
+
|
90
|
+
.sheet--left {
|
91
|
+
block-size: 100vh;
|
92
|
+
inline-size: var(--sheet-size, 288px);
|
93
|
+
inset-block-start: 0;
|
94
|
+
inset-inline-start: 0;
|
95
|
+
}
|
96
|
+
|
97
|
+
.sheet__content {
|
98
|
+
block-size: 100%;
|
99
|
+
display: flex;
|
100
|
+
flex-direction: column;
|
101
|
+
overflow-y: auto;
|
102
|
+
}
|
@@ -16,14 +16,17 @@ module ChartTableConcern
|
|
16
16
|
def setup_chart_and_table_data
|
17
17
|
ransack_params = params[:q] || {}
|
18
18
|
|
19
|
-
# Setup chart data first using original time range (no sorting from table)
|
20
19
|
unless turbo_frame_request?
|
21
|
-
|
20
|
+
# Setup chart data first using original time range (no sorting from table)
|
22
21
|
setup_chart_data(ransack_params)
|
22
|
+
setup_chart_formatters
|
23
23
|
end
|
24
24
|
|
25
25
|
# Setup table data using zoom parameters if present, otherwise use chart parameters
|
26
26
|
setup_table_data(ransack_params)
|
27
|
+
|
28
|
+
# Set flag to determine if we have meaningful data to display
|
29
|
+
@has_data = has_meaningful_data?
|
27
30
|
end
|
28
31
|
|
29
32
|
def setup_chart_data(ransack_params)
|
@@ -31,7 +34,10 @@ module ChartTableConcern
|
|
31
34
|
chart_ransack_query = chart_model.ransack(chart_ransack_params)
|
32
35
|
@chart_data = chart_class.new(
|
33
36
|
ransack_query: chart_ransack_query,
|
34
|
-
|
37
|
+
period_type: period_type,
|
38
|
+
start_time: @start_time,
|
39
|
+
end_time: @end_time,
|
40
|
+
start_duration: @start_duration,
|
35
41
|
**chart_options
|
36
42
|
).to_rails_chart
|
37
43
|
end
|
@@ -43,6 +49,7 @@ module ChartTableConcern
|
|
43
49
|
|
44
50
|
table_results = build_table_results
|
45
51
|
handle_pagination
|
52
|
+
|
46
53
|
@pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
|
47
54
|
end
|
48
55
|
|
@@ -56,14 +63,24 @@ module ChartTableConcern
|
|
56
63
|
end
|
57
64
|
|
58
65
|
def setup_chart_formatters
|
59
|
-
@xaxis_formatter = RailsPulse::ChartFormatters.
|
66
|
+
@xaxis_formatter = RailsPulse::ChartFormatters.period_as_time_or_date(@time_diff_hours)
|
60
67
|
@tooltip_formatter = RailsPulse::ChartFormatters.tooltip_as_time_or_date_with_marker(@time_diff_hours)
|
61
68
|
end
|
62
69
|
|
70
|
+
def period_type
|
71
|
+
@time_diff_hours <= 25 ? :hour : :day
|
72
|
+
end
|
73
|
+
|
63
74
|
def group_by
|
64
75
|
@time_diff_hours <= 25 ? :group_by_hour : :group_by_day
|
65
76
|
end
|
66
77
|
|
78
|
+
def has_meaningful_data?
|
79
|
+
has_chart_data = @chart_data && @chart_data.values.any? { |v| v > 0 }
|
80
|
+
has_table_data = @table_data && @table_data.any?
|
81
|
+
has_chart_data || has_table_data
|
82
|
+
end
|
83
|
+
|
67
84
|
def handle_pagination
|
68
85
|
method = pagination_method
|
69
86
|
send(method, params[:limit]) if params[:limit].present?
|
@@ -5,10 +5,13 @@ module ResponseRangeConcern
|
|
5
5
|
ransack_params = params[:q] || {}
|
6
6
|
thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
# Check both avg_duration (for Summary) and duration (for Request/Operation)
|
9
|
+
duration_param = ransack_params[:avg_duration] || ransack_params[:duration]
|
10
|
+
|
11
|
+
if duration_param.present?
|
12
|
+
selected_range = duration_param
|
10
13
|
start_duration =
|
11
|
-
case
|
14
|
+
case duration_param.to_sym
|
12
15
|
when :slow then thresholds[:slow]
|
13
16
|
when :very_slow then thresholds[:very_slow]
|
14
17
|
when :critical then thresholds[:critical]
|
@@ -6,8 +6,7 @@ module TimeRangeConcern
|
|
6
6
|
const_set(:TIME_RANGE_OPTIONS, [
|
7
7
|
[ "Last 24 hours", :last_day ],
|
8
8
|
[ "Last Week", :last_week ],
|
9
|
-
[ "Last Month", :last_month ]
|
10
|
-
[ "All Time", :all_time ]
|
9
|
+
[ "Last Month", :last_month ]
|
11
10
|
].freeze)
|
12
11
|
end
|
13
12
|
|
@@ -18,23 +17,19 @@ module TimeRangeConcern
|
|
18
17
|
|
19
18
|
ransack_params = params[:q] || {}
|
20
19
|
|
21
|
-
if ransack_params[:
|
22
|
-
# Custom time range from routes index chart zoom which filters requests through an association
|
23
|
-
start_time = parse_time_param(ransack_params[:requests_occurred_at_gteq])
|
24
|
-
end_time = parse_time_param(ransack_params[:requests_occurred_at_lt])
|
25
|
-
elsif ransack_params[:occurred_at_gteq].present?
|
20
|
+
if ransack_params[:occurred_at_gteq].present?
|
26
21
|
# Custom time range from chart zoom where there is no association
|
27
22
|
start_time = parse_time_param(ransack_params[:occurred_at_gteq])
|
28
23
|
end_time = parse_time_param(ransack_params[:occurred_at_lt])
|
29
|
-
elsif ransack_params[:
|
24
|
+
elsif ransack_params[:period_start_range]
|
30
25
|
# Predefined time range from dropdown
|
31
|
-
selected_time_range = ransack_params[:
|
26
|
+
selected_time_range = ransack_params[:period_start_range]
|
32
27
|
start_time =
|
33
28
|
case selected_time_range.to_sym
|
34
29
|
when :last_day then 1.day.ago
|
35
30
|
when :last_week then 1.week.ago
|
36
31
|
when :last_month then 1.month.ago
|
37
|
-
|
32
|
+
else 1.day.ago # Default fallback
|
38
33
|
end
|
39
34
|
end
|
40
35
|
|
@@ -2,10 +2,22 @@ module ZoomRangeConcern
|
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
4
|
def setup_zoom_range(main_start_time, main_end_time)
|
5
|
+
# Extract column selection parameter (but don't delete it yet - we need it for the view)
|
6
|
+
selected_column_time = params[:selected_column_time]
|
7
|
+
|
5
8
|
# Extract zoom parameters from params (this removes them from params)
|
6
9
|
zoom_start = params.delete(:zoom_start_time)
|
7
10
|
zoom_end = params.delete(:zoom_end_time)
|
8
11
|
|
12
|
+
# Handle column selection with highest precedence for table filtering
|
13
|
+
if selected_column_time
|
14
|
+
column_start, column_end = normalize_column_time(selected_column_time.to_i, main_start_time, main_end_time)
|
15
|
+
table_start_time = column_start
|
16
|
+
table_end_time = column_end
|
17
|
+
# Don't set zoom times for column selection - let chart show full range
|
18
|
+
return [ zoom_start, zoom_end, table_start_time, table_end_time ]
|
19
|
+
end
|
20
|
+
|
9
21
|
# Normalize zoom times to beginning/end of day or hour like we do for main time range
|
10
22
|
if zoom_start && zoom_end
|
11
23
|
zoom_start, zoom_end = normalize_zoom_times(zoom_start.to_i, zoom_end.to_i)
|
@@ -20,6 +32,25 @@ module ZoomRangeConcern
|
|
20
32
|
|
21
33
|
private
|
22
34
|
|
35
|
+
def normalize_column_time(column_time, main_start_time, main_end_time)
|
36
|
+
# Determine period type based on main time range (same logic as ChartTableConcern)
|
37
|
+
time_diff_hours = (main_end_time - main_start_time) / 3600.0
|
38
|
+
|
39
|
+
if time_diff_hours <= 25
|
40
|
+
# Hourly period - normalize to beginning/end of hour
|
41
|
+
column_time_obj = Time.zone&.at(column_time) || Time.at(column_time)
|
42
|
+
start_time = column_time_obj&.beginning_of_hour || column_time_obj
|
43
|
+
end_time = column_time_obj&.end_of_hour || column_time_obj
|
44
|
+
else
|
45
|
+
# Daily period - normalize to beginning/end of day
|
46
|
+
column_time_obj = Time.zone&.at(column_time) || Time.at(column_time)
|
47
|
+
start_time = column_time_obj&.beginning_of_day || column_time_obj
|
48
|
+
end_time = column_time_obj&.end_of_day || column_time_obj
|
49
|
+
end
|
50
|
+
|
51
|
+
[ start_time.to_i, end_time.to_i ]
|
52
|
+
end
|
53
|
+
|
23
54
|
def normalize_zoom_times(start_time, end_time)
|
24
55
|
time_diff = (end_time - start_time) / 3600.0
|
25
56
|
|
@@ -35,6 +66,6 @@ module ZoomRangeConcern
|
|
35
66
|
end_time = end_time_obj&.end_of_day || end_time_obj
|
36
67
|
end
|
37
68
|
|
38
|
-
[ start_time, end_time ]
|
69
|
+
[ start_time.to_i, end_time.to_i ]
|
39
70
|
end
|
40
71
|
end
|
@@ -2,9 +2,14 @@ module RailsPulse
|
|
2
2
|
class ApplicationController < ActionController::Base
|
3
3
|
before_action :authenticate_rails_pulse_user!
|
4
4
|
|
5
|
-
def set_pagination_limit
|
6
|
-
|
7
|
-
|
5
|
+
def set_pagination_limit(limit = nil)
|
6
|
+
limit = limit || params[:limit]
|
7
|
+
session[:pagination_limit] = limit.to_i if limit.present?
|
8
|
+
|
9
|
+
# Render JSON for direct API calls or AJAX requests (but not turbo frame requests)
|
10
|
+
if (request.xhr? && !turbo_frame_request?) || (request.patch? && action_name == "set_pagination_limit")
|
11
|
+
render json: { status: "ok" }
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
15
|
private
|
@@ -54,8 +59,11 @@ module RailsPulse
|
|
54
59
|
end
|
55
60
|
|
56
61
|
def session_pagination_limit
|
57
|
-
#
|
58
|
-
session[:pagination_limit] || 10
|
62
|
+
# Use URL param if present, otherwise session, otherwise default
|
63
|
+
limit = params[:limit].presence || session[:pagination_limit] || 10
|
64
|
+
# Update session if URL param was used
|
65
|
+
session[:pagination_limit] = limit.to_i if params[:limit].present?
|
66
|
+
limit.to_i
|
59
67
|
end
|
60
68
|
|
61
69
|
def store_pagination_limit(limit)
|
@@ -1,6 +1,18 @@
|
|
1
1
|
module RailsPulse
|
2
2
|
class DashboardController < ApplicationController
|
3
3
|
def index
|
4
|
+
@average_query_times_metric_card = RailsPulse::Routes::Cards::AverageResponseTimes.new(route: nil).to_metric_card
|
5
|
+
@percentile_response_times_metric_card = RailsPulse::Routes::Cards::PercentileResponseTimes.new(route: nil).to_metric_card
|
6
|
+
@request_count_totals_metric_card = RailsPulse::Routes::Cards::RequestCountTotals.new(route: nil).to_metric_card
|
7
|
+
@error_rate_per_route_metric_card = RailsPulse::Routes::Cards::ErrorRatePerRoute.new(route: nil).to_metric_card
|
8
|
+
|
9
|
+
# Generate chart data for inline rendering
|
10
|
+
@average_response_time_chart_data = RailsPulse::Dashboard::Charts::AverageResponseTime.new.to_chart_data
|
11
|
+
@p95_response_time_chart_data = RailsPulse::Dashboard::Charts::P95ResponseTime.new.to_chart_data
|
12
|
+
|
13
|
+
# Generate table data for inline rendering
|
14
|
+
@slow_routes_table_data = RailsPulse::Dashboard::Tables::SlowRoutes.new.to_table_data
|
15
|
+
@slow_queries_table_data = RailsPulse::Dashboard::Tables::SlowQueries.new.to_table_data
|
4
16
|
end
|
5
17
|
end
|
6
18
|
end
|
@@ -2,24 +2,60 @@ module RailsPulse
|
|
2
2
|
class QueriesController < ApplicationController
|
3
3
|
include ChartTableConcern
|
4
4
|
|
5
|
-
before_action :set_query, only: :show
|
5
|
+
before_action :set_query, only: [ :show, :analyze ]
|
6
6
|
|
7
7
|
def index
|
8
|
+
setup_metric_cards
|
8
9
|
setup_chart_and_table_data
|
9
10
|
end
|
10
11
|
|
11
12
|
def show
|
13
|
+
setup_metric_cards
|
12
14
|
setup_chart_and_table_data
|
13
15
|
end
|
14
16
|
|
17
|
+
def analyze
|
18
|
+
begin
|
19
|
+
@analysis_results = QueryAnalysisService.analyze_query(@query.id)
|
20
|
+
|
21
|
+
respond_to do |format|
|
22
|
+
format.turbo_stream {
|
23
|
+
render turbo_stream: turbo_stream.replace(
|
24
|
+
"query_analysis",
|
25
|
+
partial: "rails_pulse/queries/analysis_section",
|
26
|
+
locals: { query: @query.reload }
|
27
|
+
)
|
28
|
+
}
|
29
|
+
format.html {
|
30
|
+
redirect_to query_path(@query), notice: "Query analysis completed successfully."
|
31
|
+
}
|
32
|
+
end
|
33
|
+
rescue => e
|
34
|
+
Rails.logger.error("[QueryAnalysis] Analysis failed for query #{@query.id}: #{e.message}")
|
35
|
+
|
36
|
+
respond_to do |format|
|
37
|
+
format.turbo_stream {
|
38
|
+
render turbo_stream: turbo_stream.replace(
|
39
|
+
"query_analysis",
|
40
|
+
partial: "rails_pulse/queries/analysis_section",
|
41
|
+
locals: { query: @query, error_message: "Analysis failed: #{e.message}" }
|
42
|
+
)
|
43
|
+
}
|
44
|
+
format.html {
|
45
|
+
redirect_to query_path(@query), alert: "Query analysis failed: #{e.message}"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
15
51
|
private
|
16
52
|
|
17
53
|
def chart_model
|
18
|
-
|
54
|
+
Summary
|
19
55
|
end
|
20
56
|
|
21
57
|
def table_model
|
22
|
-
show_action? ? Operation :
|
58
|
+
show_action? ? Operation : Summary
|
23
59
|
end
|
24
60
|
|
25
61
|
def chart_class
|
@@ -31,73 +67,78 @@ module RailsPulse
|
|
31
67
|
end
|
32
68
|
|
33
69
|
def build_chart_ransack_params(ransack_params)
|
34
|
-
base_params = ransack_params.except(:s)
|
70
|
+
base_params = ransack_params.except(:s).merge(
|
71
|
+
period_start_gteq: Time.at(@start_time),
|
72
|
+
period_start_lt: Time.at(@end_time)
|
73
|
+
)
|
74
|
+
|
75
|
+
# Only add duration filter if we have a meaningful threshold
|
76
|
+
base_params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
35
77
|
|
36
78
|
if show_action?
|
37
|
-
base_params.merge(
|
38
|
-
query_id_eq: @query.id,
|
39
|
-
occurred_at_gteq: @start_time,
|
40
|
-
occurred_at_lt: @end_time,
|
41
|
-
duration_gteq: @start_duration
|
42
|
-
)
|
79
|
+
base_params.merge(summarizable_id_eq: @query.id)
|
43
80
|
else
|
44
|
-
base_params
|
45
|
-
operations_occurred_at_gteq: @start_time,
|
46
|
-
operations_occurred_at_lt: @end_time,
|
47
|
-
operations_duration_gteq: @start_duration
|
48
|
-
)
|
81
|
+
base_params
|
49
82
|
end
|
50
83
|
end
|
51
84
|
|
52
85
|
def build_table_ransack_params(ransack_params)
|
53
86
|
if show_action?
|
54
|
-
|
55
|
-
|
56
|
-
occurred_at_gteq: @table_start_time,
|
57
|
-
occurred_at_lt: @table_end_time,
|
58
|
-
|
87
|
+
# For Operation model on show page
|
88
|
+
params = ransack_params.merge(
|
89
|
+
occurred_at_gteq: Time.at(@table_start_time),
|
90
|
+
occurred_at_lt: Time.at(@table_end_time),
|
91
|
+
query_id_eq: @query.id
|
59
92
|
)
|
93
|
+
params[:duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
94
|
+
params
|
60
95
|
else
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
96
|
+
# For Summary model on index page
|
97
|
+
params = ransack_params.merge(
|
98
|
+
period_start_gteq: Time.at(@table_start_time),
|
99
|
+
period_start_lt: Time.at(@table_end_time)
|
65
100
|
)
|
101
|
+
params[:avg_duration_gteq] = @start_duration if @start_duration && @start_duration > 0
|
102
|
+
params
|
66
103
|
end
|
67
104
|
end
|
68
105
|
|
69
106
|
def default_table_sort
|
70
|
-
"occurred_at desc"
|
107
|
+
show_action? ? "occurred_at desc" : "period_start desc"
|
71
108
|
end
|
72
109
|
|
73
110
|
def build_table_results
|
74
111
|
if show_action?
|
75
|
-
|
112
|
+
# Only show operations that belong to time periods where we have query summaries
|
113
|
+
# This ensures the table data is consistent with the chart data
|
114
|
+
@ransack_query.result
|
115
|
+
.joins(<<~SQL)
|
116
|
+
INNER JOIN rails_pulse_summaries ON
|
117
|
+
rails_pulse_summaries.summarizable_id = rails_pulse_operations.query_id AND
|
118
|
+
rails_pulse_summaries.summarizable_type = 'RailsPulse::Query' AND
|
119
|
+
rails_pulse_summaries.period_type = '#{period_type}' AND
|
120
|
+
rails_pulse_operations.occurred_at >= rails_pulse_summaries.period_start AND
|
121
|
+
rails_pulse_operations.occurred_at < rails_pulse_summaries.period_end
|
122
|
+
SQL
|
123
|
+
.distinct
|
76
124
|
else
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
.group("rails_pulse_queries.id, rails_pulse_queries.normalized_sql, rails_pulse_queries.created_at, rails_pulse_queries.updated_at")
|
84
|
-
.select(
|
85
|
-
"rails_pulse_queries.*",
|
86
|
-
optimized_aggregations_sql
|
87
|
-
)
|
125
|
+
Queries::Tables::Index.new(
|
126
|
+
ransack_query: @ransack_query,
|
127
|
+
period_type: period_type,
|
128
|
+
start_time: @start_time,
|
129
|
+
params: params
|
130
|
+
).to_table
|
88
131
|
end
|
89
132
|
end
|
90
133
|
|
91
134
|
private
|
92
135
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
"MAX(rails_pulse_operations.occurred_at) AS occurred_at"
|
100
|
-
].join(", ")
|
136
|
+
def setup_metric_cards
|
137
|
+
return if turbo_frame_request?
|
138
|
+
|
139
|
+
@average_query_times_metric_card = RailsPulse::Queries::Cards::AverageQueryTimes.new(query: @query).to_metric_card
|
140
|
+
@percentile_query_times_metric_card = RailsPulse::Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
|
141
|
+
@execution_rate_metric_card = RailsPulse::Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
|
101
142
|
end
|
102
143
|
|
103
144
|
def show_action?
|
@@ -108,14 +149,33 @@ module RailsPulse
|
|
108
149
|
show_action? ? :set_pagination_limit : :store_pagination_limit
|
109
150
|
end
|
110
151
|
|
111
|
-
def
|
112
|
-
|
152
|
+
def setup_table_data(ransack_params)
|
153
|
+
table_ransack_params = build_table_ransack_params(ransack_params)
|
154
|
+
@ransack_query = table_model.ransack(table_ransack_params)
|
155
|
+
|
156
|
+
# Only apply default sort if not using Queries::Tables::Index (which handles its own sorting)
|
157
|
+
if show_action?
|
158
|
+
@ransack_query.sorts = default_table_sort if @ransack_query.sorts.empty?
|
159
|
+
end
|
160
|
+
|
161
|
+
table_results = build_table_results
|
162
|
+
handle_pagination
|
163
|
+
|
164
|
+
@pagy, @table_data = pagy(table_results, limit: session_pagination_limit)
|
165
|
+
end
|
166
|
+
|
167
|
+
def handle_pagination
|
168
|
+
method = pagination_method
|
169
|
+
send(method, params[:limit]) if params[:limit].present?
|
170
|
+
end
|
171
|
+
|
172
|
+
def setup_time_and_response_ranges
|
173
|
+
@start_time, @end_time, @selected_time_range, @time_diff_hours = setup_time_range
|
174
|
+
@start_duration, @selected_response_range = setup_duration_range(:query)
|
113
175
|
end
|
114
176
|
|
115
|
-
def
|
116
|
-
@
|
117
|
-
@percentile_response_times_card = Queries::Cards::PercentileQueryTimes.new(query: @query).to_metric_card
|
118
|
-
@execution_rate_card = Queries::Cards::ExecutionRate.new(query: @query).to_metric_card
|
177
|
+
def set_query
|
178
|
+
@query = Query.find(params[:id])
|
119
179
|
end
|
120
180
|
end
|
121
181
|
end
|