findbug 0.2.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +375 -0
- data/Rakefile +12 -0
- data/app/controllers/findbug/application_controller.rb +105 -0
- data/app/controllers/findbug/dashboard_controller.rb +93 -0
- data/app/controllers/findbug/errors_controller.rb +129 -0
- data/app/controllers/findbug/performance_controller.rb +80 -0
- data/app/jobs/findbug/alert_job.rb +40 -0
- data/app/jobs/findbug/cleanup_job.rb +132 -0
- data/app/jobs/findbug/persist_job.rb +158 -0
- data/app/models/findbug/error_event.rb +197 -0
- data/app/models/findbug/performance_event.rb +237 -0
- data/app/views/findbug/dashboard/index.html.erb +199 -0
- data/app/views/findbug/errors/index.html.erb +137 -0
- data/app/views/findbug/errors/show.html.erb +185 -0
- data/app/views/findbug/performance/index.html.erb +168 -0
- data/app/views/findbug/performance/show.html.erb +203 -0
- data/app/views/layouts/findbug/application.html.erb +601 -0
- data/lib/findbug/alerts/channels/base.rb +75 -0
- data/lib/findbug/alerts/channels/discord.rb +155 -0
- data/lib/findbug/alerts/channels/email.rb +179 -0
- data/lib/findbug/alerts/channels/slack.rb +149 -0
- data/lib/findbug/alerts/channels/webhook.rb +143 -0
- data/lib/findbug/alerts/dispatcher.rb +126 -0
- data/lib/findbug/alerts/throttler.rb +110 -0
- data/lib/findbug/background_persister.rb +142 -0
- data/lib/findbug/capture/context.rb +301 -0
- data/lib/findbug/capture/exception_handler.rb +141 -0
- data/lib/findbug/capture/exception_subscriber.rb +228 -0
- data/lib/findbug/capture/message_handler.rb +104 -0
- data/lib/findbug/capture/middleware.rb +247 -0
- data/lib/findbug/configuration.rb +381 -0
- data/lib/findbug/engine.rb +109 -0
- data/lib/findbug/performance/instrumentation.rb +336 -0
- data/lib/findbug/performance/transaction.rb +193 -0
- data/lib/findbug/processing/data_scrubber.rb +163 -0
- data/lib/findbug/rails/controller_methods.rb +152 -0
- data/lib/findbug/railtie.rb +222 -0
- data/lib/findbug/storage/circuit_breaker.rb +223 -0
- data/lib/findbug/storage/connection_pool.rb +134 -0
- data/lib/findbug/storage/redis_buffer.rb +285 -0
- data/lib/findbug/tasks/findbug.rake +167 -0
- data/lib/findbug/version.rb +5 -0
- data/lib/findbug.rb +216 -0
- data/lib/generators/findbug/install_generator.rb +67 -0
- data/lib/generators/findbug/templates/POST_INSTALL +41 -0
- data/lib/generators/findbug/templates/create_findbug_error_events.rb +44 -0
- data/lib/generators/findbug/templates/create_findbug_performance_events.rb +47 -0
- data/lib/generators/findbug/templates/initializer.rb +157 -0
- data/sig/findbug.rbs +4 -0
- metadata +251 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<h1>Errors</h1>
|
|
2
|
+
<p class="page-description">Track and manage application errors.</p>
|
|
3
|
+
|
|
4
|
+
<%# Filters - single row %>
|
|
5
|
+
<%= form_tag findbug.errors_path, method: :get, class: "filter-bar" do %>
|
|
6
|
+
<div class="filter-bar-filters">
|
|
7
|
+
<%= select_tag :status, options_for_select([
|
|
8
|
+
["Unresolved", "unresolved"],
|
|
9
|
+
["Resolved", "resolved"],
|
|
10
|
+
["Ignored", "ignored"],
|
|
11
|
+
["All Statuses", ""]
|
|
12
|
+
], params[:status]), onchange: "this.form.submit()" %>
|
|
13
|
+
|
|
14
|
+
<%= select_tag :severity, options_for_select([
|
|
15
|
+
["All Severities", ""],
|
|
16
|
+
["Error", "error"],
|
|
17
|
+
["Warning", "warning"],
|
|
18
|
+
["Info", "info"]
|
|
19
|
+
], params[:severity]), onchange: "this.form.submit()" %>
|
|
20
|
+
|
|
21
|
+
<%= select_tag :since, options_for_select([
|
|
22
|
+
["Last Hour", "1h"],
|
|
23
|
+
["Last 24 Hours", "24h"],
|
|
24
|
+
["Last 7 Days", "7d"],
|
|
25
|
+
["Last 30 Days", "30d"]
|
|
26
|
+
], params[:since]), onchange: "this.form.submit()" %>
|
|
27
|
+
|
|
28
|
+
<%= select_tag :sort, options_for_select([
|
|
29
|
+
["Most Recent", "recent"],
|
|
30
|
+
["Most Occurrences", "occurrences"],
|
|
31
|
+
["Oldest", "oldest"]
|
|
32
|
+
], params[:sort]), onchange: "this.form.submit()" %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="filter-bar-search">
|
|
36
|
+
<%= text_field_tag :search, params[:search], placeholder: "Search errors..." %>
|
|
37
|
+
<%= submit_tag "Search", class: "btn btn-secondary btn-sm" %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<%# Results count %>
|
|
42
|
+
<p class="text-muted text-sm" style="margin-bottom: 1rem;">
|
|
43
|
+
Showing <%= @errors.size %> of <%= @total_count %> errors
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<%# Error list %>
|
|
47
|
+
<div class="card">
|
|
48
|
+
<% if @errors.any? %>
|
|
49
|
+
<table class="table">
|
|
50
|
+
<thead>
|
|
51
|
+
<tr>
|
|
52
|
+
<th style="width: 50%;">Error</th>
|
|
53
|
+
<th>Status</th>
|
|
54
|
+
<th>Count</th>
|
|
55
|
+
<th>Last Seen</th>
|
|
56
|
+
<th>Actions</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
<% @errors.each do |error| %>
|
|
61
|
+
<tr id="error_<%= error.id %>">
|
|
62
|
+
<td>
|
|
63
|
+
<a href="<%= findbug.error_path(error) %>">
|
|
64
|
+
<strong class="font-mono"><%= error.exception_class %></strong>
|
|
65
|
+
</a>
|
|
66
|
+
<br>
|
|
67
|
+
<span class="text-muted text-sm"><%= truncate(error.message, length: 80) %></span>
|
|
68
|
+
<div style="margin-top: 0.25rem;">
|
|
69
|
+
<span class="badge badge-<%= error.severity %>"><%= error.severity %></span>
|
|
70
|
+
<% if error.release_version %>
|
|
71
|
+
<span class="text-muted text-xs" style="margin-left: 0.5rem;"><%= error.release_version.truncate(10) %></span>
|
|
72
|
+
<% end %>
|
|
73
|
+
<span class="text-muted text-xs" style="margin-left: 0.5rem;"><%= error.environment %></span>
|
|
74
|
+
</div>
|
|
75
|
+
</td>
|
|
76
|
+
<td>
|
|
77
|
+
<span class="badge badge-<%= error.status == 'unresolved' ? 'error' : (error.status == 'resolved' ? 'success' : 'muted') %>">
|
|
78
|
+
<%= error.status %>
|
|
79
|
+
</span>
|
|
80
|
+
</td>
|
|
81
|
+
<td>
|
|
82
|
+
<strong><%= error.occurrence_count %></strong>
|
|
83
|
+
</td>
|
|
84
|
+
<td class="text-muted text-sm">
|
|
85
|
+
<%= time_ago_in_words(error.last_seen_at) %> ago
|
|
86
|
+
</td>
|
|
87
|
+
<td>
|
|
88
|
+
<% if error.status == 'unresolved' %>
|
|
89
|
+
<%= button_to "Resolve", findbug.resolve_error_path(error), method: :post, class: "btn btn-secondary btn-sm", style: "margin-right: 4px;" %>
|
|
90
|
+
<%= button_to "Ignore", findbug.ignore_error_path(error), method: :post, class: "btn btn-ghost btn-sm" %>
|
|
91
|
+
<% else %>
|
|
92
|
+
<%= button_to "Reopen", findbug.reopen_error_path(error), method: :post, class: "btn btn-ghost btn-sm" %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
<% end %>
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
|
|
100
|
+
<%# Pagination %>
|
|
101
|
+
<% if @total_count > @per_page %>
|
|
102
|
+
<div class="pagination">
|
|
103
|
+
<% total_pages = (@total_count.to_f / @per_page).ceil %>
|
|
104
|
+
|
|
105
|
+
<% if @page > 1 %>
|
|
106
|
+
<%= link_to "Prev", findbug.errors_path(params.permit!.merge(page: @page - 1)) %>
|
|
107
|
+
<% else %>
|
|
108
|
+
<span class="disabled">Prev</span>
|
|
109
|
+
<% end %>
|
|
110
|
+
|
|
111
|
+
<% (1..total_pages).each do |p| %>
|
|
112
|
+
<% if p == @page %>
|
|
113
|
+
<span class="current"><%= p %></span>
|
|
114
|
+
<% else %>
|
|
115
|
+
<%= link_to p, findbug.errors_path(params.permit!.merge(page: p)) %>
|
|
116
|
+
<% end %>
|
|
117
|
+
<% end %>
|
|
118
|
+
|
|
119
|
+
<% if @page < total_pages %>
|
|
120
|
+
<%= link_to "Next", findbug.errors_path(params.permit!.merge(page: @page + 1)) %>
|
|
121
|
+
<% else %>
|
|
122
|
+
<span class="disabled">Next</span>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
<% else %>
|
|
127
|
+
<div class="empty-state">
|
|
128
|
+
<div class="empty-state-icon">
|
|
129
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
130
|
+
<circle cx="11" cy="11" r="8"/>
|
|
131
|
+
<path d="m21 21-4.3-4.3"/>
|
|
132
|
+
</svg>
|
|
133
|
+
</div>
|
|
134
|
+
<p>No errors found matching your filters.</p>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
137
|
+
</div>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<div class="error-header">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="error-title"><%= @error.exception_class %></h1>
|
|
4
|
+
<p class="error-message"><%= @error.message %></p>
|
|
5
|
+
<div class="error-meta">
|
|
6
|
+
<span class="badge badge-<%= @error.severity %>"><%= @error.severity.upcase %></span>
|
|
7
|
+
<span class="badge badge-<%= @error.status == 'unresolved' ? 'error' : 'success' %>">
|
|
8
|
+
<%= @error.status %>
|
|
9
|
+
</span>
|
|
10
|
+
<span class="text-muted text-sm"><%= @error.occurrence_count %> occurrences</span>
|
|
11
|
+
<span class="text-muted text-sm"><%= @error.environment %></span>
|
|
12
|
+
<% if @error.release_version %>
|
|
13
|
+
<span class="text-muted text-sm">Release: <%= @error.release_version %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
19
|
+
<% if @error.status == 'unresolved' %>
|
|
20
|
+
<%= button_to "Resolve", findbug.resolve_error_path(@error), method: :post, class: "btn btn-primary" %>
|
|
21
|
+
<%= button_to "Ignore", findbug.ignore_error_path(@error), method: :post, class: "btn btn-secondary" %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%= button_to "Reopen", findbug.reopen_error_path(@error), method: :post, class: "btn btn-secondary" %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="grid-3-1">
|
|
29
|
+
<%# Left column - Backtrace %>
|
|
30
|
+
<div>
|
|
31
|
+
<div class="card">
|
|
32
|
+
<div class="card-header">
|
|
33
|
+
<h2 class="card-title">Backtrace</h2>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="code-block" style="overflow-x: auto;">
|
|
36
|
+
<% @error.backtrace_lines.each_with_index do |line, i| %>
|
|
37
|
+
<span class="code-line" style="white-space: nowrap;">
|
|
38
|
+
<span class="code-line-number"><%= i + 1 %></span><%= line %>
|
|
39
|
+
</span>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<%# Breadcrumbs %>
|
|
45
|
+
<% if @error.breadcrumbs.any? %>
|
|
46
|
+
<div class="card">
|
|
47
|
+
<div class="card-header">
|
|
48
|
+
<h2 class="card-title">Breadcrumbs</h2>
|
|
49
|
+
</div>
|
|
50
|
+
<table class="table">
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
<th>Time</th>
|
|
54
|
+
<th>Category</th>
|
|
55
|
+
<th>Message</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<% @error.breadcrumbs.each do |crumb| %>
|
|
60
|
+
<tr>
|
|
61
|
+
<td class="text-muted text-xs">
|
|
62
|
+
<%= crumb['timestamp'] || crumb[:timestamp] %>
|
|
63
|
+
</td>
|
|
64
|
+
<td>
|
|
65
|
+
<span class="badge badge-muted"><%= crumb['category'] || crumb[:category] %></span>
|
|
66
|
+
</td>
|
|
67
|
+
<td><%= crumb['message'] || crumb[:message] %></td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% end %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<%# Right column - Context %>
|
|
77
|
+
<div>
|
|
78
|
+
<%# Timing %>
|
|
79
|
+
<div class="card">
|
|
80
|
+
<div class="card-header">
|
|
81
|
+
<h2 class="card-title">Timing</h2>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="card-content">
|
|
84
|
+
<div style="display: grid; gap: 0.75rem;">
|
|
85
|
+
<div style="display: flex; justify-content: space-between;">
|
|
86
|
+
<span class="text-muted text-sm">First seen</span>
|
|
87
|
+
<span class="text-sm local-time" data-utc="<%= @error.first_seen_at.iso8601 %>"><%= @error.first_seen_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="separator"></div>
|
|
90
|
+
<div style="display: flex; justify-content: space-between;">
|
|
91
|
+
<span class="text-muted text-sm">Last seen</span>
|
|
92
|
+
<span class="text-sm local-time" data-utc="<%= @error.last_seen_at.iso8601 %>"><%= @error.last_seen_at.strftime("%Y-%m-%d %H:%M:%S UTC") %></span>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="separator"></div>
|
|
95
|
+
<div style="display: flex; justify-content: space-between;">
|
|
96
|
+
<span class="text-muted text-sm">Total occurrences</span>
|
|
97
|
+
<strong><%= @error.occurrence_count %></strong>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<%# User Info %>
|
|
104
|
+
<% if @error.user.present? %>
|
|
105
|
+
<div class="card">
|
|
106
|
+
<div class="card-header">
|
|
107
|
+
<h2 class="card-title">User</h2>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="card-content">
|
|
110
|
+
<div style="display: grid; gap: 0.75rem;">
|
|
111
|
+
<% @error.user.each do |key, value| %>
|
|
112
|
+
<div style="display: flex; justify-content: space-between;">
|
|
113
|
+
<span class="text-muted text-sm"><%= key %></span>
|
|
114
|
+
<span class="text-sm"><%= value %></span>
|
|
115
|
+
</div>
|
|
116
|
+
<% unless key == @error.user.keys.last %><div class="separator"></div><% end %>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<% end %>
|
|
122
|
+
|
|
123
|
+
<%# Request Info %>
|
|
124
|
+
<% if @error.request.present? %>
|
|
125
|
+
<div class="card">
|
|
126
|
+
<div class="card-header">
|
|
127
|
+
<h2 class="card-title">Request</h2>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="card-content" style="overflow: hidden;">
|
|
130
|
+
<div style="display: grid; gap: 0.75rem;">
|
|
131
|
+
<% @error.request.each_with_index do |(key, value), idx| %>
|
|
132
|
+
<% next if value.blank? || (value.is_a?(Hash) && value.empty?) %>
|
|
133
|
+
<div style="overflow: hidden;">
|
|
134
|
+
<span class="text-muted text-xs" style="text-transform: uppercase; letter-spacing: 0.05em;"><%= key %></span>
|
|
135
|
+
<div class="text-sm" style="word-break: break-all; margin-top: 0.25rem; overflow: hidden;">
|
|
136
|
+
<% if value.is_a?(Hash) %>
|
|
137
|
+
<pre class="code-block" style="margin: 0; padding: 0.5rem; white-space: pre-wrap; word-break: break-word; overflow-x: auto;"><%= JSON.pretty_generate(value) %></pre>
|
|
138
|
+
<% else %>
|
|
139
|
+
<%= value.to_s %>
|
|
140
|
+
<% end %>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<% end %>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
148
|
+
|
|
149
|
+
<%# Tags %>
|
|
150
|
+
<% if @error.tags.present? && @error.tags.any? %>
|
|
151
|
+
<div class="card">
|
|
152
|
+
<div class="card-header">
|
|
153
|
+
<h2 class="card-title">Tags</h2>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="card-content" style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
|
156
|
+
<% @error.tags.each do |key, value| %>
|
|
157
|
+
<span class="badge badge-info"><%= key %>: <%= value %></span>
|
|
158
|
+
<% end %>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<% end %>
|
|
162
|
+
|
|
163
|
+
<%# Similar Errors %>
|
|
164
|
+
<% if @similar_errors.any? %>
|
|
165
|
+
<div class="card">
|
|
166
|
+
<div class="card-header">
|
|
167
|
+
<h2 class="card-title">Similar Errors</h2>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="card-content">
|
|
170
|
+
<% @similar_errors.each_with_index do |error, idx| %>
|
|
171
|
+
<div>
|
|
172
|
+
<a href="<%= findbug.error_path(error) %>" class="text-sm">
|
|
173
|
+
<%= truncate(error.message, length: 50) %>
|
|
174
|
+
</a>
|
|
175
|
+
<div class="text-muted text-xs" style="margin-top: 0.25rem;">
|
|
176
|
+
<%= error.occurrence_count %> occurrences · <%= time_ago_in_words(error.last_seen_at) %> ago
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<% unless idx == @similar_errors.length - 1 %><div class="separator"></div><% end %>
|
|
180
|
+
<% end %>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<% end %>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<h1>Performance</h1>
|
|
2
|
+
|
|
3
|
+
<%# Time filter %>
|
|
4
|
+
<div class="filters">
|
|
5
|
+
<%= form_tag findbug.performance_index_path, method: :get do %>
|
|
6
|
+
<div class="filter-group">
|
|
7
|
+
<label>Time Range</label>
|
|
8
|
+
<%= select_tag :since, options_for_select([
|
|
9
|
+
["Last Hour", "1h"],
|
|
10
|
+
["Last 24 Hours", "24h"],
|
|
11
|
+
["Last 7 Days", "7d"],
|
|
12
|
+
["Last 30 Days", "30d"]
|
|
13
|
+
], params[:since] || "24h"), onchange: "this.form.submit()" %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<%# Stats overview %>
|
|
19
|
+
<div class="stats-grid">
|
|
20
|
+
<div class="stat-card">
|
|
21
|
+
<div class="stat-label">Total Requests</div>
|
|
22
|
+
<div class="stat-value"><%= @stats[:total_requests] %></div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<div class="stat-label">Avg Duration</div>
|
|
27
|
+
<div class="stat-value <%= @stats[:avg_duration] > 500 ? 'warning' : '' %>">
|
|
28
|
+
<%= @stats[:avg_duration] %>ms
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="stat-card">
|
|
33
|
+
<div class="stat-label">Max Duration</div>
|
|
34
|
+
<div class="stat-value"><%= @stats[:max_duration] %>ms</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="stat-card">
|
|
38
|
+
<div class="stat-label">N+1 Rate</div>
|
|
39
|
+
<div class="stat-value <%= @stats[:n_plus_one_percentage] > 10 ? 'warning' : 'success' %>">
|
|
40
|
+
<%= @stats[:n_plus_one_percentage] %>%
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="grid-2">
|
|
46
|
+
<%# Slowest Endpoints %>
|
|
47
|
+
<div class="card">
|
|
48
|
+
<div class="card-header">
|
|
49
|
+
<h2 class="card-title">Slowest Endpoints</h2>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<% if @slowest.any? %>
|
|
53
|
+
<table class="table">
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>Endpoint</th>
|
|
57
|
+
<th>Avg</th>
|
|
58
|
+
<th>Max</th>
|
|
59
|
+
<th>Count</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
<% @slowest.each do |endpoint| %>
|
|
64
|
+
<tr>
|
|
65
|
+
<td>
|
|
66
|
+
<a href="<%= findbug.performance_path(endpoint[:transaction_name]) %>">
|
|
67
|
+
<%= truncate(endpoint[:transaction_name], length: 35) %>
|
|
68
|
+
</a>
|
|
69
|
+
</td>
|
|
70
|
+
<td>
|
|
71
|
+
<span class="badge badge-<%= endpoint[:avg_duration_ms] > 500 ? 'warning' : (endpoint[:avg_duration_ms] > 200 ? 'info' : 'success') %>">
|
|
72
|
+
<%= endpoint[:avg_duration_ms].round %>ms
|
|
73
|
+
</span>
|
|
74
|
+
</td>
|
|
75
|
+
<td class="text-muted">
|
|
76
|
+
<%= endpoint[:max_duration_ms].round %>ms
|
|
77
|
+
</td>
|
|
78
|
+
<td><%= endpoint[:count] %></td>
|
|
79
|
+
</tr>
|
|
80
|
+
<% end %>
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
<% else %>
|
|
84
|
+
<div class="empty-state">
|
|
85
|
+
<div class="empty-state-icon">📊</div>
|
|
86
|
+
<p>No performance data yet</p>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<%# N+1 Hotspots %>
|
|
92
|
+
<div class="card">
|
|
93
|
+
<div class="card-header">
|
|
94
|
+
<h2 class="card-title">N+1 Hotspots</h2>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<% if @n_plus_one.any? %>
|
|
98
|
+
<table class="table">
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th>Endpoint</th>
|
|
102
|
+
<th>N+1 Count</th>
|
|
103
|
+
<th>Avg Queries</th>
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
<% @n_plus_one.each do |endpoint| %>
|
|
108
|
+
<tr>
|
|
109
|
+
<td>
|
|
110
|
+
<a href="<%= findbug.performance_path(endpoint[:transaction_name]) %>">
|
|
111
|
+
<%= truncate(endpoint[:transaction_name], length: 35) %>
|
|
112
|
+
</a>
|
|
113
|
+
</td>
|
|
114
|
+
<td>
|
|
115
|
+
<span class="badge badge-warning"><%= endpoint[:n_plus_one_count] %></span>
|
|
116
|
+
</td>
|
|
117
|
+
<td><%= endpoint[:avg_queries] %></td>
|
|
118
|
+
</tr>
|
|
119
|
+
<% end %>
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
<% else %>
|
|
123
|
+
<div class="empty-state">
|
|
124
|
+
<div class="empty-state-icon">✓</div>
|
|
125
|
+
<p>No N+1 issues detected</p>
|
|
126
|
+
</div>
|
|
127
|
+
<% end %>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<%# Throughput chart %>
|
|
132
|
+
<div class="card">
|
|
133
|
+
<div class="card-header">
|
|
134
|
+
<h2 class="card-title">Throughput Over Time</h2>
|
|
135
|
+
<span class="text-muted text-xs">Requests per interval</span>
|
|
136
|
+
</div>
|
|
137
|
+
<% if @throughput.any? %>
|
|
138
|
+
<div class="card-content">
|
|
139
|
+
<div class="chart-container">
|
|
140
|
+
<% max_count = [@throughput.map { |t| t[:count] }.max.to_f, 1].max %>
|
|
141
|
+
<% @throughput.each do |point| %>
|
|
142
|
+
<% height = (point[:count] / max_count * 100).round %>
|
|
143
|
+
<% height = [height, 3].max if point[:count] > 0 %>
|
|
144
|
+
<div
|
|
145
|
+
class="chart-bar"
|
|
146
|
+
style="height: <%= height %>%;"
|
|
147
|
+
data-tooltip="<%= point[:time].strftime('%H:%M') %> - <%= point[:count] %> req, <%= point[:avg_duration_ms]&.round || 0 %>ms"
|
|
148
|
+
></div>
|
|
149
|
+
<% end %>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="chart-labels text-muted text-xs">
|
|
152
|
+
<span><%= @throughput.first[:time].strftime("%H:%M") if @throughput.first %></span>
|
|
153
|
+
<span><%= @throughput.last[:time].strftime("%H:%M") if @throughput.last %></span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<% else %>
|
|
157
|
+
<div class="empty-state">
|
|
158
|
+
<div class="empty-state-icon">
|
|
159
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
160
|
+
<line x1="12" y1="20" x2="12" y2="10"/>
|
|
161
|
+
<line x1="18" y1="20" x2="18" y2="4"/>
|
|
162
|
+
<line x1="6" y1="20" x2="6" y2="16"/>
|
|
163
|
+
</svg>
|
|
164
|
+
</div>
|
|
165
|
+
<p>Not enough data to display chart</p>
|
|
166
|
+
</div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|