rails_pulse 0.2.2 → 0.2.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/app/assets/stylesheets/rails_pulse/components/tags.css +2 -2
- data/app/controllers/concerns/chart_table_concern.rb +2 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -0
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -8
- data/app/controllers/rails_pulse/queries_controller.rb +12 -7
- data/app/controllers/rails_pulse/requests_controller.rb +8 -4
- data/app/controllers/rails_pulse/routes_controller.rb +13 -6
- data/app/models/concerns/rails_pulse/taggable.rb +63 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +12 -5
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +12 -5
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +7 -0
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +6 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +10 -6
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +16 -10
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +10 -6
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +5 -2
- data/app/models/rails_pulse/queries/tables/index.rb +12 -2
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +12 -6
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +10 -6
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +10 -6
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +10 -6
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +10 -6
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +9 -5
- data/app/models/rails_pulse/routes/tables/index.rb +12 -2
- data/app/models/rails_pulse/summary.rb +55 -0
- data/app/views/layouts/rails_pulse/_global_filters.html.erb +9 -2
- data/app/views/rails_pulse/components/_active_filters.html.erb +36 -0
- data/app/views/rails_pulse/components/_page_header.html.erb +4 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +4 -0
- data/app/views/rails_pulse/queries/index.html.erb +1 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/index.html.erb +1 -1
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +2 -2
- data/config/initializers/rails_charts_csp_patch.rb +9 -9
- data/lib/rails_pulse/cleanup_service.rb +8 -0
- data/lib/rails_pulse/engine.rb +25 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- metadata +4 -3
- data/app/models/concerns/taggable.rb +0 -61
|
@@ -2,8 +2,10 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Cards
|
|
4
4
|
class RequestCountTotals
|
|
5
|
-
def initialize(route: nil)
|
|
5
|
+
def initialize(route: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@route = route
|
|
7
|
+
@disabled_tags = disabled_tags
|
|
8
|
+
@show_non_tagged = show_non_tagged
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
def to_metric_card
|
|
@@ -11,11 +13,13 @@ module RailsPulse
|
|
|
11
13
|
previous_7_days = 14.days.ago.beginning_of_day
|
|
12
14
|
|
|
13
15
|
# Single query to get all count metrics with conditional aggregation
|
|
14
|
-
base_query = RailsPulse::Summary
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
base_query = RailsPulse::Summary
|
|
17
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
18
|
+
.where(
|
|
19
|
+
summarizable_type: "RailsPulse::Route",
|
|
20
|
+
period_type: "day",
|
|
21
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
|
22
|
+
)
|
|
19
23
|
base_query = base_query.where(summarizable_id: @route.id) if @route
|
|
20
24
|
|
|
21
25
|
metrics = base_query.select(
|
|
@@ -2,20 +2,24 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Charts
|
|
4
4
|
class AverageResponseTimes
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, route: nil, start_time: nil, end_time: nil, start_duration: nil)
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, route: nil, start_time: nil, end_time: nil, start_duration: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@route = route
|
|
9
9
|
@start_time = start_time
|
|
10
10
|
@end_time = end_time
|
|
11
11
|
@start_duration = start_duration
|
|
12
|
+
@disabled_tags = disabled_tags
|
|
13
|
+
@show_non_tagged = show_non_tagged
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def to_rails_chart
|
|
15
|
-
summaries = @ransack_query.result(distinct: false)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
summaries = @ransack_query.result(distinct: false)
|
|
18
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
19
|
+
.where(
|
|
20
|
+
summarizable_type: "RailsPulse::Route",
|
|
21
|
+
period_type: @period_type
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
summaries = summaries.where(summarizable_id: @route.id) if @route
|
|
21
25
|
summaries = summaries
|
|
@@ -2,12 +2,13 @@ module RailsPulse
|
|
|
2
2
|
module Routes
|
|
3
3
|
module Tables
|
|
4
4
|
class Index
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, start_time:, params:, disabled_tags: [])
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@start_time = start_time
|
|
9
9
|
@params = params
|
|
10
10
|
@disabled_tags = disabled_tags
|
|
11
|
+
@show_non_tagged = show_non_tagged
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_table
|
|
@@ -24,10 +25,19 @@ module RailsPulse
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
# Apply tag filters by excluding routes with disabled tags
|
|
27
|
-
|
|
28
|
+
# Separate "non_tagged" from actual tags (it's a virtual tag)
|
|
29
|
+
actual_disabled_tags = @disabled_tags.reject { |tag| tag == "non_tagged" }
|
|
30
|
+
|
|
31
|
+
# Exclude routes with actual disabled tags
|
|
32
|
+
actual_disabled_tags.each do |tag|
|
|
28
33
|
base_query = base_query.where.not("rails_pulse_routes.tags LIKE ?", "%#{tag}%")
|
|
29
34
|
end
|
|
30
35
|
|
|
36
|
+
# Exclude non-tagged routes if show_non_tagged is false
|
|
37
|
+
unless @show_non_tagged
|
|
38
|
+
base_query = base_query.where("rails_pulse_routes.tags IS NOT NULL AND rails_pulse_routes.tags != '[]'")
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
base_query = base_query.where(summarizable_id: @route.id) if @route
|
|
32
42
|
|
|
33
43
|
# Apply grouping and aggregation
|
|
@@ -33,6 +33,61 @@ module RailsPulse
|
|
|
33
33
|
where(summarizable_type: "RailsPulse::Request", summarizable_id: 0)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
# Tag filtering scope for charts and metrics
|
|
37
|
+
# Filters summaries based on disabled tags in the underlying route/query
|
|
38
|
+
scope :with_tag_filters, ->(disabled_tags = [], show_non_tagged = true) {
|
|
39
|
+
# Separate "non_tagged" from actual tags (it's a virtual tag)
|
|
40
|
+
actual_disabled_tags = disabled_tags.reject { |tag| tag == "non_tagged" }
|
|
41
|
+
|
|
42
|
+
# Return early if no filters are applied
|
|
43
|
+
return all if actual_disabled_tags.empty? && show_non_tagged
|
|
44
|
+
|
|
45
|
+
# Determine which table to join based on summarizable_type
|
|
46
|
+
# We need to handle both Route and Query summaries
|
|
47
|
+
relation = all
|
|
48
|
+
|
|
49
|
+
# Filter route summaries
|
|
50
|
+
route_ids = RailsPulse::Route.all
|
|
51
|
+
|
|
52
|
+
# Exclude routes with disabled tags
|
|
53
|
+
actual_disabled_tags.each do |tag|
|
|
54
|
+
route_ids = route_ids.where.not("tags LIKE ?", "%#{tag}%")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Exclude non-tagged routes if show_non_tagged is false
|
|
58
|
+
route_ids = route_ids.where("tags IS NOT NULL AND tags != '[]'") unless show_non_tagged
|
|
59
|
+
|
|
60
|
+
route_ids = route_ids.pluck(:id)
|
|
61
|
+
|
|
62
|
+
# Filter query summaries
|
|
63
|
+
query_ids = RailsPulse::Query.all
|
|
64
|
+
|
|
65
|
+
# Exclude queries with disabled tags
|
|
66
|
+
actual_disabled_tags.each do |tag|
|
|
67
|
+
query_ids = query_ids.where.not("tags LIKE ?", "%#{tag}%")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Exclude non-tagged queries if show_non_tagged is false
|
|
71
|
+
query_ids = query_ids.where("tags IS NOT NULL AND tags != '[]'") unless show_non_tagged
|
|
72
|
+
|
|
73
|
+
query_ids = query_ids.pluck(:id)
|
|
74
|
+
|
|
75
|
+
# Apply filters: include only summaries for filtered routes/queries
|
|
76
|
+
# If no routes/queries match the filter, we need to ensure nothing is returned
|
|
77
|
+
# Use -1 as an impossible ID instead of 0 (which might be used for aggregates)
|
|
78
|
+
relation = relation.where(
|
|
79
|
+
"(" \
|
|
80
|
+
" (summarizable_type = 'RailsPulse::Route' AND summarizable_id IN (?)) OR " \
|
|
81
|
+
" (summarizable_type = 'RailsPulse::Query' AND summarizable_id IN (?)) OR " \
|
|
82
|
+
" (summarizable_type = 'RailsPulse::Request')" \
|
|
83
|
+
")",
|
|
84
|
+
route_ids.presence || [ -1 ],
|
|
85
|
+
query_ids.presence || [ -1 ]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
relation
|
|
89
|
+
}
|
|
90
|
+
|
|
36
91
|
# Ransack configuration
|
|
37
92
|
def self.ransackable_attributes(auth_object = nil)
|
|
38
93
|
%w[
|
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
<div data-rails-pulse--global-filters-target="wrapper" data-action="click->rails-pulse--global-filters#closeOnClickOutside" style="display: none; position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 1000; align-items: center; justify-content: center;">
|
|
16
16
|
<div class="dialog" data-rails-pulse--global-filters-target="dialog" style="position: relative; opacity: 1; transform: scale(1);">
|
|
17
17
|
<div class="dialog__content">
|
|
18
|
-
<
|
|
19
|
-
|
|
18
|
+
<div class="flex items-start justify-between">
|
|
19
|
+
<div>
|
|
20
|
+
<h2 class="text-lg font-semibold mbe-4">Global Filters</h2>
|
|
21
|
+
<p class="text-sm text-subtle">Set default time filters that persist across all pages. These can be overridden by page-specific filters.</p>
|
|
22
|
+
</div>
|
|
23
|
+
<%= link_to '#', "aria-label": "Close", data: { action: "rails-pulse--global-filters#close" }, class: "text-subtle hover:text-default" do %>
|
|
24
|
+
<%= rails_pulse_icon 'x', width: '20' %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
20
27
|
|
|
21
28
|
<%= form_with url: settings_global_filters_path, method: :patch, local: true, data: { action: "submit->rails-pulse--global-filters#submit" } do |form| %>
|
|
22
29
|
<div class="flex flex-col gap mb-4">
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% global_filters = session_global_filters %>
|
|
2
|
+
<% disabled_tags = global_filters['disabled_tags'] || [] %>
|
|
3
|
+
<% has_date_filters = global_filters['start_time'].present? && global_filters['end_time'].present? %>
|
|
4
|
+
<% has_performance_filter = global_filters['performance_threshold'].present? %>
|
|
5
|
+
<% has_tag_filters = disabled_tags.any? || session[:show_non_tagged] == false %>
|
|
6
|
+
<% has_any_filters = has_date_filters || has_performance_filter || has_tag_filters %>
|
|
7
|
+
|
|
8
|
+
<% if has_any_filters %>
|
|
9
|
+
<div class="flex items-center gap text-sm">
|
|
10
|
+
Filtered:
|
|
11
|
+
<% if has_date_filters %>
|
|
12
|
+
<% start_time = Time.parse(global_filters['start_time']) %>
|
|
13
|
+
<% end_time = Time.parse(global_filters['end_time']) %>
|
|
14
|
+
<span class="badge badge--secondary"><%= start_time.strftime("%b %d, %Y %-I:%M %p") %> - <%= end_time.strftime("%b %d, %Y %-I:%M %p") %></span>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% if has_performance_filter %>
|
|
18
|
+
<% threshold_label = case global_filters['performance_threshold']
|
|
19
|
+
when 'slow' then 'Slow and Above'
|
|
20
|
+
when 'very_slow' then 'Very Slow and Above'
|
|
21
|
+
when 'critical' then 'Critical'
|
|
22
|
+
else global_filters['performance_threshold'].titleize
|
|
23
|
+
end %>
|
|
24
|
+
<span class="badge badge--secondary"><%= threshold_label %></span>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<% if has_tag_filters %>
|
|
28
|
+
<% disabled_tags.each do |tag| %>
|
|
29
|
+
<span class="badge badge--secondary"><%= tag.humanize %></span>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% if session[:show_non_tagged] == false %>
|
|
32
|
+
<span class="badge badge--secondary">Non tagged hidden</span>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
@@ -16,5 +16,9 @@
|
|
|
16
16
|
<div class="breadcrumb-tags">
|
|
17
17
|
<%= render 'rails_pulse/tags/tag_manager', taggable: taggable %>
|
|
18
18
|
</div>
|
|
19
|
+
<% elsif defined?(show_active_filters) && show_active_filters %>
|
|
20
|
+
<div class="breadcrumb-tags">
|
|
21
|
+
<%= render 'rails_pulse/components/active_filters' %>
|
|
22
|
+
</div>
|
|
19
23
|
<% end %>
|
|
20
24
|
</div>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<div class="flex justify-end mb-1">
|
|
2
|
+
<%= render 'rails_pulse/components/active_filters' %>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
1
5
|
<div class="row">
|
|
2
6
|
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @average_query_times_metric_card } %>
|
|
3
7
|
<%= render 'rails_pulse/components/metric_card', { class: "grid-item block", data: @percentile_response_times_metric_card } %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
>
|
|
12
12
|
<div class="tag-list">
|
|
13
13
|
<% current_tags.each do |tag| %>
|
|
14
|
-
<span class="
|
|
14
|
+
<span class="badge badge--secondary font-normal">
|
|
15
15
|
<%= tag %>
|
|
16
16
|
<%= button_to remove_tag_path(taggable_type, taggable.id, tag: tag),
|
|
17
17
|
method: :delete,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<button
|
|
30
30
|
type="button"
|
|
31
31
|
id="tag_menu_button_<%= taggable_type %>_<%= taggable.id %>"
|
|
32
|
-
class="
|
|
32
|
+
class="badge badge--positive tag-add-button font-normal"
|
|
33
33
|
data-rails-pulse--popover-target="button"
|
|
34
34
|
data-action="rails-pulse--popover#toggle"
|
|
35
35
|
aria-haspopup="true"
|
|
@@ -61,15 +61,15 @@ if defined?(RailsCharts)
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
# Apply the patch to
|
|
64
|
+
# Apply the CSP patch only to Rails Pulse helpers, not the entire application
|
|
65
|
+
# By prepending to ChartHelper instead of ApplicationHelper, we scope the patch to RailsPulse
|
|
66
|
+
# namespace only, avoiding conflicts with any chart libraries in the host application
|
|
67
|
+
# (Chartkick, Highcharts, Google Charts, ApexCharts, custom helpers, etc.)
|
|
65
68
|
Rails.application.config.to_prepare do
|
|
66
|
-
if defined?(RailsCharts)
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if defined?(RailsPulse::ApplicationHelper)
|
|
72
|
-
RailsPulse::ApplicationHelper.prepend(RailsCharts::CspPatch)
|
|
73
|
-
end
|
|
69
|
+
if defined?(RailsCharts) && defined?(RailsPulse::ChartHelper)
|
|
70
|
+
# Prepend CSP patch to RailsPulse::ChartHelper
|
|
71
|
+
# This wraps only the rails_charts methods, ensuring clean CSP nonce injection
|
|
72
|
+
# without affecting the host application's chart helpers
|
|
73
|
+
RailsPulse::ChartHelper.prepend(RailsCharts::CspPatch)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
@@ -69,6 +69,11 @@ module RailsPulse
|
|
|
69
69
|
return 0 unless defined?(RailsPulse::Request)
|
|
70
70
|
|
|
71
71
|
count = RailsPulse::Request.where("occurred_at < ?", cutoff_time).count
|
|
72
|
+
request_ids = RailsPulse::Request.where("occurred_at < ?", cutoff_time).pluck(:id)
|
|
73
|
+
|
|
74
|
+
# Delete associated operations first to respect foreign key constraints
|
|
75
|
+
RailsPulse::Operation.where(request_id: request_ids).delete_all
|
|
76
|
+
|
|
72
77
|
RailsPulse::Request.where("occurred_at < ?", cutoff_time).delete_all
|
|
73
78
|
count
|
|
74
79
|
end
|
|
@@ -140,6 +145,9 @@ module RailsPulse
|
|
|
140
145
|
.limit(records_to_delete)
|
|
141
146
|
.pluck(:id)
|
|
142
147
|
|
|
148
|
+
# Delete associated operations first to respect foreign key constraints
|
|
149
|
+
RailsPulse::Operation.where(request_id: ids_to_delete).delete_all
|
|
150
|
+
|
|
143
151
|
RailsPulse::Request.where(id: ids_to_delete).delete_all
|
|
144
152
|
records_to_delete
|
|
145
153
|
end
|
data/lib/rails_pulse/engine.rb
CHANGED
|
@@ -14,6 +14,17 @@ module RailsPulse
|
|
|
14
14
|
class Engine < ::Rails::Engine
|
|
15
15
|
isolate_namespace RailsPulse
|
|
16
16
|
|
|
17
|
+
# Prevent rails_charts from polluting the global ActionView namespace
|
|
18
|
+
# This MUST happen before any initializers run to avoid conflicts with host apps
|
|
19
|
+
# that use Chartkick or other chart libraries
|
|
20
|
+
if defined?(RailsCharts::Engine)
|
|
21
|
+
# Find and remove the rails_charts.helpers initializer
|
|
22
|
+
RailsCharts::Engine.initializers.delete_if do |init|
|
|
23
|
+
init.name == "rails_charts.helpers"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
# Load Rake tasks
|
|
18
29
|
rake_tasks do
|
|
19
30
|
Dir.glob(File.expand_path("../tasks/**/*.rake", __FILE__)).each { |file| load file }
|
|
@@ -50,6 +61,20 @@ module RailsPulse
|
|
|
50
61
|
RailsCharts.options[:theme] = "railspulse"
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# Manually include RailsCharts helpers only in RailsPulse views
|
|
65
|
+
# This ensures rails_charts methods are only available in RailsPulse namespace,
|
|
66
|
+
# not in the host application
|
|
67
|
+
initializer "rails_pulse.include_rails_charts_helpers" do
|
|
68
|
+
ActiveSupport.on_load :action_view do
|
|
69
|
+
if defined?(RailsCharts::Helpers) && defined?(RailsPulse::ChartHelper)
|
|
70
|
+
unless RailsPulse::ChartHelper.include?(RailsCharts::Helpers)
|
|
71
|
+
RailsPulse::ChartHelper.include(RailsCharts::Helpers)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
53
78
|
initializer "rails_pulse.ransack", after: "ransack.initialize" do
|
|
54
79
|
# Ensure Ransack is loaded before our models
|
|
55
80
|
end
|
data/lib/rails_pulse/version.rb
CHANGED