rails_pulse 0.3.3 → 0.4.0.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/CHANGELOG.md +299 -0
- data/README.md +44 -3
- data/app/assets/stylesheets/rails_pulse/application.css +21 -32
- data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
- data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
- data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
- data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
- data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
- data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
- data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
- data/app/controllers/concerns/chart_table_concern.rb +15 -3
- data/app/controllers/concerns/ransack_params_concern.rb +18 -0
- data/app/controllers/concerns/response_range_concern.rb +1 -1
- data/app/controllers/concerns/time_range_concern.rb +28 -14
- data/app/controllers/rails_pulse/application_controller.rb +104 -31
- data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
- data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
- data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
- data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
- data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +1 -1
- data/app/controllers/rails_pulse/routes_controller.rb +7 -1
- data/app/controllers/rails_pulse/storage_controller.rb +7 -0
- data/app/controllers/rails_pulse/tags_controller.rb +2 -2
- data/app/helpers/rails_pulse/application_helper.rb +1 -0
- data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
- data/app/helpers/rails_pulse/chart_helper.rb +0 -3
- data/app/helpers/rails_pulse/route_helper.rb +8 -16
- data/app/javascript/rails_pulse/application.js +29 -8
- data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
- data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
- data/app/models/concerns/rails_pulse/taggable.rb +11 -6
- data/app/models/rails_pulse/cards/base.rb +53 -0
- data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
- data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
- data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
- data/app/models/rails_pulse/deployment.rb +47 -1
- data/app/models/rails_pulse/exception_group.rb +37 -0
- data/app/models/rails_pulse/exception_occurrence.rb +27 -0
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
- data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
- data/app/models/rails_pulse/operation.rb +13 -4
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
- data/app/models/rails_pulse/request.rb +5 -1
- data/app/models/rails_pulse/route.rb +71 -21
- data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
- data/app/models/rails_pulse/routes/tables/index.rb +8 -6
- data/app/models/rails_pulse/summary.rb +21 -10
- data/app/models/rails_pulse/tables/base.rb +4 -2
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
- data/app/services/rails_pulse/exception_capture_service.rb +228 -0
- data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
- data/app/services/rails_pulse/operations/change_point.rb +144 -0
- data/app/services/rails_pulse/operations/compare.rb +92 -0
- data/app/services/rails_pulse/operations/comparison.rb +126 -0
- data/app/services/rails_pulse/operations/metric.rb +63 -0
- data/app/services/rails_pulse/operations/series.rb +84 -0
- data/app/services/rails_pulse/operations/subject.rb +77 -0
- data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
- data/app/services/rails_pulse/route_merger.rb +99 -0
- data/app/services/rails_pulse/route_migrator.rb +139 -0
- data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
- data/app/services/rails_pulse/route_recognizer.rb +47 -0
- data/app/services/rails_pulse/summary_service.rb +45 -0
- data/app/services/rails_pulse/tag_filter_service.rb +5 -3
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
- data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
- data/app/views/rails_pulse/components/_panel.html.erb +16 -6
- data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
- data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
- data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
- data/app/views/rails_pulse/deployments/index.html.erb +15 -0
- data/app/views/rails_pulse/deployments/show.html.erb +62 -0
- data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
- data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
- data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
- data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
- data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
- data/app/views/rails_pulse/requests/_table.html.erb +1 -1
- data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
- data/app/views/rails_pulse/routes/_table.html.erb +22 -8
- data/app/views/rails_pulse/routes/index.html.erb +5 -1
- data/app/views/rails_pulse/routes/show.html.erb +5 -0
- data/app/views/rails_pulse/storage/show.html.erb +123 -0
- data/config/routes.rb +13 -4
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
- data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
- data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
- data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
- data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
- data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
- data/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/base_methods.rb +2 -0
- data/lib/generators/rails_pulse/install_generator.rb +5 -0
- data/lib/generators/rails_pulse/schema_parser.rb +22 -2
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
- data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
- data/lib/rails_pulse/cleanup_service.rb +110 -15
- data/lib/rails_pulse/configuration.rb +125 -6
- data/lib/rails_pulse/engine.rb +32 -17
- data/lib/rails_pulse/installers/config_updater.rb +202 -0
- data/lib/rails_pulse/job_run_collector.rb +43 -16
- data/lib/rails_pulse/middleware/asset_server.rb +11 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -4
- data/lib/rails_pulse/packaged_assets.rb +147 -0
- data/lib/rails_pulse/route_indexes.rb +66 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
- data/lib/rails_pulse/tracker.rb +29 -10
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +1 -0
- data/lib/rails_pulse_server.ru +16 -47
- data/lib/tasks/rails_pulse.rake +83 -4
- data/lib/tasks/rails_pulse_assets.rake +15 -0
- data/lib/tasks/rails_pulse_tasks.rake +29 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +18 -117
- metadata +78 -37
- data/Rakefile +0 -554
- data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
- data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
- data/app/javascript/rails_pulse/theme.js +0 -416
- data/config/brakeman.ignore +0 -190
- data/config/brakeman.yml +0 -68
- data/config/initializers/rails_pulse.rb +0 -287
- data/public/rails-pulse-assets/csp-test.js +0 -120
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
- data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
- data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
- data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
- data/vendor/assets/javascripts/rails-pulse.js +0 -157
- data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
- data/vendor/assets/stylesheets/rails-pulse.css +0 -1
- data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<%= render 'rails_pulse/components/page_header', show_active_filters: true %>
|
|
2
|
+
|
|
3
|
+
<%= render 'rails_pulse/exceptions/metric_cards' %>
|
|
4
|
+
|
|
5
|
+
<%= render 'rails_pulse/components/panel', {
|
|
6
|
+
title: 'Exceptions',
|
|
7
|
+
card_classes: 'b-full',
|
|
8
|
+
help_heading: 'Exception Frequency',
|
|
9
|
+
help_text: 'How often exceptions were raised over the selected period, across every group. Read from retained summaries rather than individual occurrence records, so the history stays accurate after cleanup has pruned old occurrences.'
|
|
10
|
+
} do %>
|
|
11
|
+
<div class="filter-bar">
|
|
12
|
+
<%= search_form_for @ransack_query, url: exceptions_path, class: "filter-bar__form" do |form| %>
|
|
13
|
+
<%= form.search_field :exception_class_cont,
|
|
14
|
+
placeholder: "Filter by exception class",
|
|
15
|
+
autocomplete: "off",
|
|
16
|
+
class: "input"
|
|
17
|
+
%>
|
|
18
|
+
<%= form.select :status_eq,
|
|
19
|
+
options_for_select(
|
|
20
|
+
[["All", ""]] + RailsPulse::ExceptionGroup::STATUSES.map { |s| [s.capitalize, s] },
|
|
21
|
+
@ransack_query.status_eq
|
|
22
|
+
),
|
|
23
|
+
{},
|
|
24
|
+
{ class: "input" }
|
|
25
|
+
%>
|
|
26
|
+
<%= if params.has_key?(:q)
|
|
27
|
+
link_to "Reset",
|
|
28
|
+
exceptions_path,
|
|
29
|
+
class: "btn btn--plain",
|
|
30
|
+
style: "text-decoration: none; color: var(--color-text-muted);"
|
|
31
|
+
end %>
|
|
32
|
+
<%= form.submit "Search", class: "btn" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if @table_data.any? %>
|
|
37
|
+
<% if @occurrence_volume_chart %>
|
|
38
|
+
<%# No chart tabs on this page — the toggles list holds the deploy toggle alone. %>
|
|
39
|
+
<div class="panel-tabs">
|
|
40
|
+
<div class="panel-tabs__list">
|
|
41
|
+
<%= render "rails_pulse/shared/deployment_markers_toggle" %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="chart-container chart-container--slim">
|
|
46
|
+
<%= render_stimulus_chart(
|
|
47
|
+
@occurrence_volume_chart,
|
|
48
|
+
type: "bar",
|
|
49
|
+
id: "exception_occurrence_volume_chart",
|
|
50
|
+
height: "100%",
|
|
51
|
+
options: bar_chart_options.deep_merge(series: { barMaxWidth: 48 })
|
|
52
|
+
) %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<div data-controller="rails-pulse--table-sort">
|
|
57
|
+
<div id="index_table" data-rails-pulse--table-sort-target="tableFrame">
|
|
58
|
+
<%= render 'rails_pulse/exceptions/table' %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<% else %>
|
|
62
|
+
<%= render 'rails_pulse/components/empty_state',
|
|
63
|
+
title: 'No exceptions recorded.',
|
|
64
|
+
description: 'Exceptions raised by your application will appear here.' %>
|
|
65
|
+
<% end %>
|
|
66
|
+
<% end %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%= render 'rails_pulse/components/page_header' %>
|
|
2
|
+
|
|
3
|
+
<% status_actions = [
|
|
4
|
+
{ label: "Resolve", url: exception_path(@exception_group), method: :patch, params: { status: "resolved" }, class: "btn", disabled: @exception_group.status == "resolved" },
|
|
5
|
+
{ label: "Ignore", url: exception_path(@exception_group), method: :patch, params: { status: "ignored" }, class: "btn", disabled: @exception_group.status == "ignored" },
|
|
6
|
+
{ label: "Open", url: exception_path(@exception_group), method: :patch, params: { status: "open" }, class: "btn", disabled: @exception_group.status == "open" },
|
|
7
|
+
{ label: @exception_group.preserve? ? "Unpreserve" : "Preserve",
|
|
8
|
+
url: exception_path(@exception_group), method: :patch,
|
|
9
|
+
params: { preserve: !@exception_group.preserve? }, class: "btn" }
|
|
10
|
+
] %>
|
|
11
|
+
<%= render 'rails_pulse/components/panel', title: 'Exception Details', actions: status_actions do %>
|
|
12
|
+
<dl class="descriptive-list mbs-4">
|
|
13
|
+
<dt>Class</dt>
|
|
14
|
+
<dd><code><%= @exception_group.exception_class %></code></dd>
|
|
15
|
+
<dt>Location</dt>
|
|
16
|
+
<dd><code><%= @exception_group.location.presence || "—" %></code></dd>
|
|
17
|
+
<dt>Message</dt>
|
|
18
|
+
<dd><%= @exception_group.message.presence || "—" %></dd>
|
|
19
|
+
<dt>Status</dt>
|
|
20
|
+
<dd><%= render 'rails_pulse/exceptions/status_badge', group: @exception_group %></dd>
|
|
21
|
+
<dt>First Seen</dt>
|
|
22
|
+
<dd><%= human_readable_occurred_at(@exception_group.first_seen_at) %></dd>
|
|
23
|
+
<dt>Last Seen</dt>
|
|
24
|
+
<dd><%= human_readable_occurred_at(@exception_group.last_seen_at) %></dd>
|
|
25
|
+
<dt>Total Seen</dt>
|
|
26
|
+
<dd><%= number_with_delimiter(@exception_group.occurrence_count) %></dd>
|
|
27
|
+
<dt>Stored Occurrences</dt>
|
|
28
|
+
<dd><%= number_with_delimiter(@exception_group.occurrences.count) %></dd>
|
|
29
|
+
<dt>Preserve</dt>
|
|
30
|
+
<dd><%= @exception_group.preserve? ? "Yes — exempt from cleanup" : "No" %></dd>
|
|
31
|
+
</dl>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<% if @latest_occurrence&.backtrace.present? %>
|
|
35
|
+
<%= render 'rails_pulse/components/panel', title: 'Most Recent Backtrace' do %>
|
|
36
|
+
<%= render 'rails_pulse/exceptions/backtrace', backtrace: @latest_occurrence.backtrace %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<%= render 'rails_pulse/components/panel', title: 'Occurrences' do %>
|
|
41
|
+
<% if @occurrences.any? %>
|
|
42
|
+
<%= render 'rails_pulse/exceptions/occurrences_table' %>
|
|
43
|
+
<% else %>
|
|
44
|
+
<%= render 'rails_pulse/components/empty_state',
|
|
45
|
+
title: 'No occurrences found.',
|
|
46
|
+
description: '' %>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% end %>
|
|
49
|
+
|
|
50
|
+
<%= render 'rails_pulse/components/raw_data',
|
|
51
|
+
title: 'Raw Data',
|
|
52
|
+
content: RailsPulse::Exceptions::RawData.new(@exception_group, occurrence: @latest_occurrence).to_s %>
|
|
@@ -58,26 +58,4 @@
|
|
|
58
58
|
json_output = JSON.pretty_generate(raw_data.compact)
|
|
59
59
|
%>
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
data-controller="rails-pulse--collapsible"
|
|
63
|
-
data-rails-pulse--collapsible-collapsed-class="collapsed"
|
|
64
|
-
class="collapsible-code"
|
|
65
|
-
>
|
|
66
|
-
<h2 class="grow font-semibold leading-none mbe-1 uppercase text-xs">
|
|
67
|
-
Raw Data
|
|
68
|
-
<button
|
|
69
|
-
type="button"
|
|
70
|
-
class="collapsible-toggle"
|
|
71
|
-
data-rails-pulse--collapsible-target="toggle"
|
|
72
|
-
data-action="click->rails-pulse--collapsible#toggle"
|
|
73
|
-
>show</button>
|
|
74
|
-
<button
|
|
75
|
-
type="button"
|
|
76
|
-
class="collapsible-toggle"
|
|
77
|
-
onclick="navigator.clipboard.writeText(this.closest('.collapsible-code').querySelector('code').textContent).then(() => { this.textContent = 'copied!'; setTimeout(() => this.textContent = 'copy', 1500) })"
|
|
78
|
-
>copy</button>
|
|
79
|
-
</h2>
|
|
80
|
-
<div class="prose max-i-none" data-rails-pulse--collapsible-target="content">
|
|
81
|
-
<pre class="mbs-0" style="white-space: pre; margin-block-end: 0; margin-block-start: 0;"><code><%= html_escape(json_output) %></code></pre>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
61
|
+
<%= render 'rails_pulse/components/raw_data', content: json_output %>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
%>
|
|
24
24
|
<tr>
|
|
25
25
|
<td class="truncate-cell">
|
|
26
|
-
<%= link_to
|
|
26
|
+
<%= link_to request.route.path_and_method, route_path(request.route) %>
|
|
27
27
|
</td>
|
|
28
28
|
<td class="whitespace-nowrap">
|
|
29
29
|
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request) %>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<% columns = [
|
|
2
2
|
{ field: :occurred_at, label: 'Timestamp', class: 'w-auto' },
|
|
3
|
+
{ field: :http_method, label: 'Method', class: 'w-20' },
|
|
3
4
|
{ field: :duration, label: 'Response Time', class: 'w-36' },
|
|
4
5
|
{ field: :status, label: 'Status', class: 'w-20' },
|
|
5
6
|
{ field: nil, label: 'Tags', class: 'w-32' }
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
<td class="whitespace-nowrap">
|
|
15
16
|
<%= link_to human_readable_occurred_at(request.occurred_at), request_path(request) %>
|
|
16
17
|
</td>
|
|
18
|
+
<td class="whitespace-nowrap">
|
|
19
|
+
<% verb = request.method %>
|
|
20
|
+
<span class="badge badge--<%= verb&.downcase %>"><%= verb %></span>
|
|
21
|
+
</td>
|
|
17
22
|
<td class="whitespace-nowrap">
|
|
18
23
|
<% performance_class = case request.duration
|
|
19
24
|
when 0..100 then "text-green-600"
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<% columns = [
|
|
2
|
-
{ field: :route_path,
|
|
3
|
-
{ field: :
|
|
4
|
-
{ field: :
|
|
5
|
-
{ field: :
|
|
6
|
-
{ field:
|
|
2
|
+
{ field: :route_path, label: "Path", class: "w-auto", cell_class: "truncate-cell" },
|
|
3
|
+
{ field: :route_methods_sort, label: "Methods", class: "w-32" },
|
|
4
|
+
{ field: :route_controller_action, label: "Action", class: "w-48", cell_class: "truncate-cell" },
|
|
5
|
+
{ field: :p95_duration_sort, label: "P95", class: "w-32" },
|
|
6
|
+
{ field: :p99_duration_sort, label: "P99", class: "w-32" },
|
|
7
|
+
{ field: :count_sort, label: "Requests", class: "w-24" },
|
|
8
|
+
{ field: nil, label: "Tags", class: "w-32" },
|
|
7
9
|
] %>
|
|
8
10
|
|
|
9
11
|
<table class="table table-fixed mbs-4">
|
|
@@ -12,9 +14,21 @@
|
|
|
12
14
|
<tbody>
|
|
13
15
|
<% @table_data.each do |summary| %>
|
|
14
16
|
<tr>
|
|
15
|
-
<td class="truncate-cell"
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
<td class="truncate-cell">
|
|
18
|
+
<%= link_to route_path(summary.route_id), class: 'no-underline' do %>
|
|
19
|
+
<%= summary.path %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="whitespace-nowrap">
|
|
23
|
+
<% (JSON.parse(summary.route_methods || "[]") rescue []).sort.each do |verb| %>
|
|
24
|
+
<span class="badge badge--<%= verb.downcase %>"><%= verb %></span>
|
|
25
|
+
<% end %>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="truncate-cell text-muted"><%= summary.controller_action %></td>
|
|
28
|
+
<td class="whitespace-nowrap"><%= summary.p95_duration.to_i %>
|
|
29
|
+
ms</td>
|
|
30
|
+
<td class="whitespace-nowrap"><%= summary.p99_duration.to_i %>
|
|
31
|
+
ms</td>
|
|
18
32
|
<td class="whitespace-nowrap"><%= number_with_delimiter summary.count %></td>
|
|
19
33
|
<td class="whitespace-nowrap"><%= display_tag_badges(summary.tags) %></td>
|
|
20
34
|
</tr>
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
data-rails-pulse--chart-switcher-target="chartTypeInput"
|
|
26
26
|
>
|
|
27
27
|
<%= form.search_field :route_path_cont,
|
|
28
|
-
placeholder: "Filter by
|
|
28
|
+
placeholder: "Filter by path",
|
|
29
|
+
autocomplete: "off",
|
|
30
|
+
class: "input" %>
|
|
31
|
+
<%= form.search_field :route_controller_action_cont,
|
|
32
|
+
placeholder: "Filter by action",
|
|
29
33
|
autocomplete: "off",
|
|
30
34
|
class: "input" %>
|
|
31
35
|
<%= form.select :avg_duration,
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
{ selected: @selected_response_range },
|
|
30
30
|
{ class: "input" }
|
|
31
31
|
%>
|
|
32
|
+
<%= form.select :http_method_eq,
|
|
33
|
+
[ [ "All Methods", "" ] ] + @route.http_methods_list.sort.map { |m| [ m, m ] },
|
|
34
|
+
{ selected: params.dig(:q, :http_method_eq) },
|
|
35
|
+
{ class: "input" }
|
|
36
|
+
%>
|
|
32
37
|
<%= link_to "Reset", route_path(@route), class: "btn btn--plain", style: "text-decoration: none; color: var(--color-text-muted);" if params.has_key?(:q) %>
|
|
33
38
|
<%= form.submit "Search", class: "btn" %>
|
|
34
39
|
<% end %>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<%= render "rails_pulse/components/page_header" %>
|
|
2
|
+
|
|
3
|
+
<div class="row">
|
|
4
|
+
<div class="card storage-overview-card">
|
|
5
|
+
<p class="font-semibold uppercase text-xs text-subtle mbe-1">Highest fill</p>
|
|
6
|
+
<p class="text-xl font-bold"><% if @status.overview[:hottest_percent] %><%= @status.overview[:hottest_percent].round %>%<% else %>--<% end %></p>
|
|
7
|
+
<p class="text-xs text-subtle"><%= @status.overview[:hottest_label] %></p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="card storage-overview-card">
|
|
10
|
+
<p class="font-semibold uppercase text-xs text-subtle mbe-1">Records</p>
|
|
11
|
+
<p class="text-xl font-bold"><%= number_with_delimiter(@status.overview[:total_records]) %></p>
|
|
12
|
+
<p class="text-xs text-subtle">Across <%= @status.tables.size %> tables</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="card storage-overview-card">
|
|
15
|
+
<p class="font-semibold uppercase text-xs text-subtle mbe-1">Disk used</p>
|
|
16
|
+
<p class="text-xl font-bold"><%= @status.overview[:display_bytes] ? human_readable_bytes(@status.overview[:display_bytes]) : "—" %></p>
|
|
17
|
+
<p class="text-xs text-subtle"><%= @status.database[:adapter] %> · <%= @status.database[:setup_label] %></p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card storage-overview-card">
|
|
20
|
+
<p class="font-semibold uppercase text-xs text-subtle mbe-1">Cleanup</p>
|
|
21
|
+
<p class="text-xl font-bold storage-health storage-health--<%= @status.overview[:cleanup_health] %>"><%= @status.overview[:cleanup_label] %></p>
|
|
22
|
+
<p class="text-xs text-subtle">Retain <%= @status.cleanup[:retention_label] %></p>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<% if @status.pressure_items.any? %>
|
|
27
|
+
<div class="card">
|
|
28
|
+
<div class="flex items-center mbe-3">
|
|
29
|
+
<h2 class="font-semibold leading-none uppercase text-xs">Needs attention</h2>
|
|
30
|
+
<span class="badge badge--negative mis-2 text-xs"><%= @status.pressure_items.size %></span>
|
|
31
|
+
</div>
|
|
32
|
+
<% @status.pressure_items.each do |item| %>
|
|
33
|
+
<%= render "rails_pulse/dashboard/attention_item", item: item, severity: item[:severity].to_s %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<%= render "rails_pulse/components/panel",
|
|
39
|
+
title: "Tables",
|
|
40
|
+
help_heading: "Count caps",
|
|
41
|
+
help_text: "Fill is measured against max_table_records. Cleanup deletes oldest rows after the retention window, then trims down to these caps. Tables with no cap grow until time-based cleanup (if any) removes them." do %>
|
|
42
|
+
<div class="storage-tables mbs-4">
|
|
43
|
+
<% @status.tables.each do |table| %>
|
|
44
|
+
<div class="storage-table">
|
|
45
|
+
<div class="storage-table__header">
|
|
46
|
+
<div>
|
|
47
|
+
<h3 class="font-semibold text-sm"><%= table[:label] %></h3>
|
|
48
|
+
<p class="text-xs text-subtle"><%= table[:description] %></p>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="storage-table__counts">
|
|
51
|
+
<span class="font-semibold text-sm"><%= number_with_delimiter(table[:count]) %></span>
|
|
52
|
+
<% if table[:limit] %>
|
|
53
|
+
<span class="text-xs text-subtle"> / <%= number_with_delimiter(table[:limit]) %></span>
|
|
54
|
+
<span class="badge storage-table__percent storage-table__percent--<%= table[:severity] %>"><%= table[:percent_label] %></span>
|
|
55
|
+
<% else %>
|
|
56
|
+
<span class="badge storage-table__percent storage-table__percent--uncapped">No cap</span>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="storage-fill" aria-hidden="true">
|
|
61
|
+
<div class="storage-fill__bar storage-fill__bar--<%= table[:severity] %>" style="width: <%= table[:bar_width] %>%"></div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="storage-table__meta text-xs text-subtle">
|
|
64
|
+
<span><%= table[:history_label] %></span>
|
|
65
|
+
<span><%= number_with_delimiter(table[:recent_count]) %> in last 24h</span>
|
|
66
|
+
<span><%= table[:runway_label] %></span>
|
|
67
|
+
<span><%= table[:bytes] ? human_readable_bytes(table[:bytes]) : "Size unknown" %></span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
73
|
+
|
|
74
|
+
<div class="row">
|
|
75
|
+
<%= render "rails_pulse/components/panel",
|
|
76
|
+
title: "Cleanup",
|
|
77
|
+
help_heading: "How cleanup works",
|
|
78
|
+
help_text: "Time-based cleanup deletes records older than full_retention_period, but only after that period has been summarised. Count-based cleanup then trims each table down to max_table_records. Schedule RailsPulse::CleanupJob daily." do %>
|
|
79
|
+
<dl class="descriptive-list mbs-4">
|
|
80
|
+
<dt>Automatic cleanup</dt>
|
|
81
|
+
<dd><%= @status.cleanup[:enabled] ? "Enabled" : "Disabled (archiving_enabled = false)" %></dd>
|
|
82
|
+
<dt>Retention period</dt>
|
|
83
|
+
<dd><%= @status.cleanup[:retention_label] %></dd>
|
|
84
|
+
<dt>Last hourly summary</dt>
|
|
85
|
+
<dd>
|
|
86
|
+
<% if @status.cleanup[:last_summary_at] %>
|
|
87
|
+
<%= human_readable_occurred_at(@status.cleanup[:last_summary_at]) %>
|
|
88
|
+
<span class="text-subtle">(<%= time_ago_in_words(@status.cleanup[:last_summary_at]) %>)</span>
|
|
89
|
+
<% else %>
|
|
90
|
+
Never — cleanup will not delete unsummarised requests
|
|
91
|
+
<% end %>
|
|
92
|
+
</dd>
|
|
93
|
+
<dt>Count caps</dt>
|
|
94
|
+
<dd>Configured in <code>max_table_records</code>. Tables without a key have no count limit.</dd>
|
|
95
|
+
</dl>
|
|
96
|
+
<% end %>
|
|
97
|
+
|
|
98
|
+
<%= render "rails_pulse/components/panel",
|
|
99
|
+
title: "Database",
|
|
100
|
+
help_heading: "What size means",
|
|
101
|
+
help_text: "On PostgreSQL and MySQL this is Pulse table size including indexes. On a dedicated SQLite database this is the file size. Shared SQLite cannot report a Pulse-only file size." do %>
|
|
102
|
+
<dl class="descriptive-list mbs-4">
|
|
103
|
+
<dt>Adapter</dt>
|
|
104
|
+
<dd><%= @status.database[:adapter] %></dd>
|
|
105
|
+
<dt>Setup</dt>
|
|
106
|
+
<dd><%= @status.database[:setup_label] %></dd>
|
|
107
|
+
<dt>Database</dt>
|
|
108
|
+
<dd><code><%= @status.database[:database_name].presence || "—" %></code></dd>
|
|
109
|
+
<dt>Size</dt>
|
|
110
|
+
<dd>
|
|
111
|
+
<% if @status.overview[:display_bytes] %>
|
|
112
|
+
<%= human_readable_bytes(@status.overview[:display_bytes]) %>
|
|
113
|
+
<% else %>
|
|
114
|
+
Not available
|
|
115
|
+
<% end %>
|
|
116
|
+
</dd>
|
|
117
|
+
<% if @status.database[:size_note] %>
|
|
118
|
+
<dt>Note</dt>
|
|
119
|
+
<dd class="text-subtle"><%= @status.database[:size_note] %></dd>
|
|
120
|
+
<% end %>
|
|
121
|
+
</dl>
|
|
122
|
+
<% end %>
|
|
123
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -9,12 +9,18 @@ RailsPulse::Engine.routes.draw do
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
resources :operations, only: %i[show]
|
|
12
|
+
if RailsPulse.configuration.track_exceptions
|
|
13
|
+
resources :exceptions, only: %i[index show update] do
|
|
14
|
+
resources :occurrences, only: %i[show], controller: "exception_occurrences"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
12
17
|
|
|
13
18
|
if RailsPulse.configuration.track_jobs
|
|
14
19
|
resources :jobs, only: %i[index show], param: :id do
|
|
15
20
|
resources :runs, only: %i[index show], controller: "job_runs"
|
|
16
21
|
end
|
|
17
22
|
end
|
|
23
|
+
resource :storage, only: :show, controller: "storage"
|
|
18
24
|
patch "pagination/limit", to: "application#set_pagination_limit", as: :pagination_limit
|
|
19
25
|
patch "settings/global_filters", to: "application#set_global_filters", as: :settings_global_filters
|
|
20
26
|
patch "settings/time_range", to: "application#set_time_range", as: :settings_time_range
|
|
@@ -23,15 +29,18 @@ RailsPulse::Engine.routes.draw do
|
|
|
23
29
|
post "tags/:taggable_type/:taggable_id/add", to: "tags#create", as: :add_tag
|
|
24
30
|
delete "tags/:taggable_type/:taggable_id/remove", to: "tags#destroy", as: :remove_tag
|
|
25
31
|
|
|
26
|
-
#
|
|
27
|
-
|
|
32
|
+
# Deployments are both an API endpoint for CI/CD and a browsable record of
|
|
33
|
+
# what shipped when.
|
|
34
|
+
resources :deployments, only: %i[index show create] do
|
|
28
35
|
collection do
|
|
29
36
|
put :finish
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
|
|
33
|
-
# CSP compliance testing
|
|
34
|
-
|
|
40
|
+
# CSP compliance testing (development/test only)
|
|
41
|
+
if Rails.env.local?
|
|
42
|
+
get "csp_test", to: "csp_test#show", as: :csp_test
|
|
43
|
+
end
|
|
35
44
|
|
|
36
45
|
# Asset serving fallback
|
|
37
46
|
get "rails-pulse-assets/:asset_name", to: "assets#show", as: :asset, constraints: { asset_name: /.*/ }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class CreateRailsPulseExceptions < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
unless table_exists?(:rails_pulse_exception_groups)
|
|
4
|
+
create_table :rails_pulse_exception_groups do |t|
|
|
5
|
+
t.string :fingerprint, null: false, comment: "SHA256 of exception_class + relative first app-code location"
|
|
6
|
+
t.string :exception_class, null: false, comment: "e.g. ActiveRecord::RecordNotFound"
|
|
7
|
+
t.string :location, comment: "Relative first app-code frame, e.g. app/models/user.rb#save"
|
|
8
|
+
t.text :message, comment: "Message from the most recent occurrence"
|
|
9
|
+
t.datetime :first_seen_at, null: false
|
|
10
|
+
t.datetime :last_seen_at, null: false
|
|
11
|
+
t.integer :occurrence_count, null: false, default: 0
|
|
12
|
+
t.string :status, null: false, default: "open", comment: "open, resolved, ignored"
|
|
13
|
+
t.datetime :resolved_at, comment: "When the group was last resolved"
|
|
14
|
+
t.boolean :preserve, null: false, default: false, comment: "Exempt from all automatic cleanup including occurrence rows"
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :rails_pulse_exception_groups, :fingerprint, unique: true, name: "index_rp_exception_groups_on_fingerprint"
|
|
19
|
+
add_index :rails_pulse_exception_groups, :last_seen_at, name: "index_rp_exception_groups_on_last_seen_at"
|
|
20
|
+
add_index :rails_pulse_exception_groups, :exception_class, name: "index_rp_exception_groups_on_class"
|
|
21
|
+
add_index :rails_pulse_exception_groups, :status, name: "index_rp_exception_groups_on_status"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless table_exists?(:rails_pulse_exception_occurrences)
|
|
25
|
+
create_table :rails_pulse_exception_occurrences do |t|
|
|
26
|
+
t.references :exception_group, null: false, index: false,
|
|
27
|
+
foreign_key: { to_table: :rails_pulse_exception_groups },
|
|
28
|
+
comment: "FK to the group this occurrence belongs to"
|
|
29
|
+
t.string :exception_class, null: false
|
|
30
|
+
t.text :message
|
|
31
|
+
t.text :backtrace, comment: "JSON array of {file, line, method} frames (first 50)"
|
|
32
|
+
t.string :request_url, comment: "Nullable — web requests only"
|
|
33
|
+
t.string :request_method, comment: "GET, POST, etc."
|
|
34
|
+
t.string :environment, comment: "production, staging, etc."
|
|
35
|
+
t.string :deploy_sha, comment: "Captured now even though Pro uses it — cannot backfill later"
|
|
36
|
+
t.text :request_params, comment: "JSON hash of filtered request params — web requests only"
|
|
37
|
+
t.datetime :occurred_at, null: false
|
|
38
|
+
t.timestamps
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
add_index :rails_pulse_exception_occurrences, :occurred_at, name: "index_rp_exception_occurrences_on_occurred_at"
|
|
42
|
+
add_index :rails_pulse_exception_occurrences, :exception_group_id, name: "index_rp_exception_occurrences_on_group_id"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def down
|
|
47
|
+
drop_table :rails_pulse_exception_occurrences if table_exists?(:rails_pulse_exception_occurrences)
|
|
48
|
+
drop_table :rails_pulse_exception_groups if table_exists?(:rails_pulse_exception_groups)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -11,11 +11,16 @@ class AddActualQueryToOperations < ActiveRecord::Migration[7.0]
|
|
|
11
11
|
# NOTE: On MySQL installations that hit the 255-char truncation bug,
|
|
12
12
|
# actual_sql will contain a partial SQL string — the full query is unrecoverable.
|
|
13
13
|
# The associated query.normalized_sql is unaffected and analysis still works.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
#
|
|
15
|
+
# Uses execute() rather than the model so the backfill runs on the migration's own
|
|
16
|
+
# connection and sees the column added above, even on separate-database setups where
|
|
17
|
+
# RailsPulse::ApplicationRecord uses a different connection pool (SQLite keeps DDL
|
|
18
|
+
# inside the open transaction, so a second pool cannot see the new column).
|
|
19
|
+
execute(<<~SQL)
|
|
20
|
+
UPDATE rails_pulse_operations
|
|
21
|
+
SET actual_sql = label
|
|
22
|
+
WHERE operation_type = 'sql' AND actual_sql IS NULL
|
|
23
|
+
SQL
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def down
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddControllerActionToRailsPulseRoutes < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
unless column_exists?(:rails_pulse_routes, :controller_action)
|
|
4
|
+
add_column :rails_pulse_routes, :controller_action, :string,
|
|
5
|
+
comment: "Rails controller and action handling this route (e.g., admin/users#show)"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|