watchdog-dashboard 0.1.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/MIT-LICENSE +20 -0
- data/README.md +123 -0
- data/Rakefile +8 -0
- data/app/assets/config/watchdog_dashboard_manifest.js +1 -0
- data/app/assets/stylesheets/watchdog/dashboard/application.css +15 -0
- data/app/controllers/concerns/flaky_stats.rb +217 -0
- data/app/controllers/concerns/metric_stats.rb +217 -0
- data/app/controllers/watchdog/dashboard/application_controller.rb +6 -0
- data/app/controllers/watchdog/dashboard/dashboard_controller.rb +71 -0
- data/app/controllers/watchdog/dashboard/metrics_controller.rb +37 -0
- data/app/helpers/watchdog/dashboard/application_helper.rb +6 -0
- data/app/helpers/watchdog/dashboard/dashboard_helper.rb +4 -0
- data/app/helpers/watchdog/dashboard/metrics_helper.rb +4 -0
- data/app/jobs/watchdog/dashboard/application_job.rb +6 -0
- data/app/mailers/watchdog/dashboard/application_mailer.rb +8 -0
- data/app/models/watchdog/dashboard/application_record.rb +7 -0
- data/app/models/watchdog/dashboard/metric.rb +5 -0
- data/app/views/layouts/watchdog/dashboard/application.html.erb +28 -0
- data/app/views/watchdog/dashboard/dashboard/_sidebar.html.erb +83 -0
- data/app/views/watchdog/dashboard/dashboard/flakies.html.erb +263 -0
- data/app/views/watchdog/dashboard/dashboard/historic.html.erb +495 -0
- data/app/views/watchdog/dashboard/dashboard/index.html.erb +219 -0
- data/app/views/watchdog/dashboard/dashboard/metrics.html.erb +263 -0
- data/config/initializers/assets.rb +1 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20250331204354_create_watchdog_dashboard_metrics.rb +14 -0
- data/lib/tasks/watchdog/dashboard_tasks.rake +4 -0
- data/lib/watchdog/dashboard/engine.rb +7 -0
- data/lib/watchdog/dashboard/version.rb +5 -0
- data/lib/watchdog/dashboard.rb +19 -0
- metadata +92 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module Watchdog::Dashboard
|
2
|
+
class DashboardController < ApplicationController
|
3
|
+
include MetricStats
|
4
|
+
include FlakyStats
|
5
|
+
|
6
|
+
def index
|
7
|
+
end
|
8
|
+
|
9
|
+
def historic
|
10
|
+
@metrics = Metric.all
|
11
|
+
|
12
|
+
@run_time_distribution = run_time_distribution
|
13
|
+
@performance_trend = performance_trend
|
14
|
+
@test_count_trend = test_count_trend
|
15
|
+
@longest_tests_by_day = longest_tests_by_day
|
16
|
+
@total_execution_time_by_day = total_execution_time_by_day
|
17
|
+
@tests_exceeding_time_threshold = tests_exceeding_time_threshold
|
18
|
+
@failed_tests_trend_by_file = failed_tests_trend_by_file
|
19
|
+
@avg_execution_time_by_file = avg_execution_time_by_file
|
20
|
+
@stability_trend = stability_trend
|
21
|
+
end
|
22
|
+
|
23
|
+
def metrics
|
24
|
+
return redirect_to root_path if Metric.count.zero?
|
25
|
+
|
26
|
+
@metrics = Metric.all
|
27
|
+
@average_time = average_time
|
28
|
+
@fastest_test = fastest_test
|
29
|
+
@slowest_test = slowest_test
|
30
|
+
@percentiles = percentiles
|
31
|
+
@failed_tests = failed_tests
|
32
|
+
@tests_grouped_by_file = tests_grouped_by_file
|
33
|
+
@tests_that_took_longer_than = tests_that_took_longer_than(0.5)
|
34
|
+
@time_distribution_analysis = time_distribution_analysis
|
35
|
+
|
36
|
+
@test_stability_analysis = test_stability_analysis
|
37
|
+
@total_tests = @test_stability_analysis[:total_tests]
|
38
|
+
@passed_percentage = @test_stability_analysis[:passed_percentage]
|
39
|
+
@failed_percentage = @test_stability_analysis[:failed_percentage]
|
40
|
+
@pending_percentage = @test_stability_analysis[:pending_percentage]
|
41
|
+
|
42
|
+
@execution_time_variance = execution_time_variance
|
43
|
+
@temporal_complexity_analysis = temporal_complexity_analysis
|
44
|
+
@test_dependency_analysis = test_dependency_analysis
|
45
|
+
end
|
46
|
+
|
47
|
+
def flakies
|
48
|
+
return redirect_to root_path if Metric.count.zero?
|
49
|
+
|
50
|
+
@metrics = Metric.all
|
51
|
+
@average_time = flaky_average_time
|
52
|
+
@fastest_test = flaky_fastest_test
|
53
|
+
@slowest_test = flaky_slowest_test
|
54
|
+
@percentiles = flaky_percentiles
|
55
|
+
@failed_tests = flaky_failed_tests
|
56
|
+
@tests_grouped_by_file = flaky_tests_grouped_by_file
|
57
|
+
@tests_that_took_longer_than = flaky_tests_that_took_longer_than(0.5)
|
58
|
+
@time_distribution_analysis = flaky_time_distribution_analysis
|
59
|
+
|
60
|
+
@test_stability_analysis = flaky_test_stability_analysis
|
61
|
+
@total_tests = @test_stability_analysis[:total_tests]
|
62
|
+
@passed_percentage = @test_stability_analysis[:passed_percentage]
|
63
|
+
@failed_percentage = @test_stability_analysis[:failed_percentage]
|
64
|
+
@pending_percentage = @test_stability_analysis[:pending_percentage]
|
65
|
+
|
66
|
+
@execution_time_variance = flaky_execution_time_variance
|
67
|
+
@temporal_complexity_analysis = flaky_temporal_complexity_analysis
|
68
|
+
@test_dependency_analysis = flaky_test_dependency_analysis
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Watchdog::Dashboard
|
2
|
+
class MetricsController < ApplicationController
|
3
|
+
skip_forgery_protection
|
4
|
+
before_action :authenticate_token
|
5
|
+
|
6
|
+
def analytics
|
7
|
+
batch = params[:metrics]
|
8
|
+
|
9
|
+
metrics_to_insert = batch.map do |metric|
|
10
|
+
Metric.create!(
|
11
|
+
description: metric[:description],
|
12
|
+
file_path: metric[:file_path],
|
13
|
+
location: metric[:location],
|
14
|
+
run_time: metric[:run_time],
|
15
|
+
status: metric[:status],
|
16
|
+
error_message: metric[:error_message],
|
17
|
+
flaky: metric[:flaky]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def metric_params
|
25
|
+
params.require(:metric).permit(:description, :file_path, :location, :run_time, :status, :error_message, :flaky)
|
26
|
+
end
|
27
|
+
|
28
|
+
def authenticate_token
|
29
|
+
expected_token = Watchdog::Dashboard.config.watchdog_api_token
|
30
|
+
request_token = request.headers["Authorization"]
|
31
|
+
|
32
|
+
unless request_token.present? && ActiveSupport::SecurityUtils.secure_compare(request_token, expected_token)
|
33
|
+
render json: { error: "Unauthorized" }, status: :unauthorized
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Watchdog Dashboard</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<%= stylesheet_link_tag "watchdog/dashboard/application", media: "all" %>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
|
14
|
+
<style>
|
15
|
+
body {
|
16
|
+
font-family: monospace;
|
17
|
+
margin: 0;
|
18
|
+
background-image: linear-gradient(to bottom right, rgb(17, 24, 39), rgb(12, 74, 110));
|
19
|
+
color: rgb(229, 231, 235);
|
20
|
+
display: flex;
|
21
|
+
}
|
22
|
+
</style>
|
23
|
+
|
24
|
+
<%= render "sidebar" %>
|
25
|
+
<%= yield %>
|
26
|
+
|
27
|
+
</body>
|
28
|
+
</html>
|
@@ -0,0 +1,83 @@
|
|
1
|
+
<div class="sidebar">
|
2
|
+
<div class="sidebar-header">
|
3
|
+
<h2>📊 RSpec Dashboard</h2>
|
4
|
+
</div>
|
5
|
+
<ul class="sidebar-menu">
|
6
|
+
<li class="<%= 'active' if controller_name == 'dashboard' && action_name == 'index' %>">
|
7
|
+
<%= link_to "📌 Summary", root_path, class: "active" %>
|
8
|
+
</li>
|
9
|
+
<li class="<%= 'active' if controller_name == 'dashboard' && action_name == 'metrics' %>">
|
10
|
+
<%= link_to "📈 Metrics", metrics_path %>
|
11
|
+
</li>
|
12
|
+
<li class="<%= 'active' if controller_name == 'dashboard' && action_name == 'flakies' %>">
|
13
|
+
<%= link_to "🔥 Flaky Tests", flakies_path %>
|
14
|
+
</li>
|
15
|
+
<li class="<%= 'active' if controller_name == 'dashboard' && action_name == 'historic' %>">
|
16
|
+
<%= link_to "📜 Historic", historic_path %>
|
17
|
+
</li>
|
18
|
+
</ul>
|
19
|
+
</div>
|
20
|
+
<style>
|
21
|
+
/* Estilos generales */
|
22
|
+
|
23
|
+
|
24
|
+
/* Sidebar */
|
25
|
+
.sidebar {
|
26
|
+
width: 260px;
|
27
|
+
height: 100vh;
|
28
|
+
background: #1e293b;
|
29
|
+
color: white;
|
30
|
+
padding: 20px;
|
31
|
+
flex-shrink: 0;
|
32
|
+
display: flex;
|
33
|
+
flex-direction: column;
|
34
|
+
box-shadow: 3px 0 10px rgba(0, 0, 0, 0.1);
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Header del Sidebar */
|
38
|
+
.sidebar-header {
|
39
|
+
text-align: center;
|
40
|
+
font-size: 18px;
|
41
|
+
font-weight: bold;
|
42
|
+
padding: 10px;
|
43
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
44
|
+
}
|
45
|
+
|
46
|
+
/* Menú del Sidebar */
|
47
|
+
.sidebar-menu {
|
48
|
+
list-style: none;
|
49
|
+
padding: 0;
|
50
|
+
margin-top: 20px;
|
51
|
+
}
|
52
|
+
|
53
|
+
.sidebar-menu li {
|
54
|
+
margin: 12px 0;
|
55
|
+
}
|
56
|
+
|
57
|
+
.sidebar-menu li a {
|
58
|
+
display: block;
|
59
|
+
padding: 12px 16px;
|
60
|
+
color: white;
|
61
|
+
text-decoration: none;
|
62
|
+
border-radius: 8px;
|
63
|
+
transition: all 0.3s ease;
|
64
|
+
font-size: 16px;
|
65
|
+
}
|
66
|
+
|
67
|
+
.sidebar-menu li a:hover {
|
68
|
+
background: rgba(255, 255, 255, 0.2);
|
69
|
+
}
|
70
|
+
|
71
|
+
/* Estilo para la opción activa */
|
72
|
+
.sidebar-menu .active a {
|
73
|
+
background: #3b82f6;
|
74
|
+
font-weight: bold;
|
75
|
+
}
|
76
|
+
|
77
|
+
/* Contenido del Dashboard */
|
78
|
+
.dashboard-content {
|
79
|
+
flex: 1;
|
80
|
+
padding: 20px;
|
81
|
+
margin-left: 260px;
|
82
|
+
}
|
83
|
+
</style>
|
@@ -0,0 +1,263 @@
|
|
1
|
+
<style>
|
2
|
+
.container {
|
3
|
+
max-width: 1200px;
|
4
|
+
margin: 0 auto;
|
5
|
+
padding: 32px;
|
6
|
+
}
|
7
|
+
.header {
|
8
|
+
display: flex;
|
9
|
+
align-items: center;
|
10
|
+
justify-content: space-between;
|
11
|
+
margin-bottom: 1.5rem;
|
12
|
+
}
|
13
|
+
.title {
|
14
|
+
font-size: 1.875rem;
|
15
|
+
color: rgb(74, 222, 128);
|
16
|
+
}
|
17
|
+
.version {
|
18
|
+
font-size: 0.875rem;
|
19
|
+
color: rgb(156, 163, 175);
|
20
|
+
margin-left: 0.5rem;
|
21
|
+
}
|
22
|
+
.last-scan {
|
23
|
+
background-color: rgba(31, 41, 55, 0.7);
|
24
|
+
padding: 0.25rem 0.75rem;
|
25
|
+
border-radius: 0.375rem;
|
26
|
+
border-left: 2px solid rgb(239, 68, 68);
|
27
|
+
font-size: 0.75rem;
|
28
|
+
}
|
29
|
+
.last-scan-time {
|
30
|
+
color: rgb(250, 204, 21);
|
31
|
+
}
|
32
|
+
.summary-grid {
|
33
|
+
display: grid;
|
34
|
+
grid-template-columns: repeat(4, 1fr);
|
35
|
+
gap: 1.5rem;
|
36
|
+
margin-bottom: 2rem;
|
37
|
+
}
|
38
|
+
.summary-card {
|
39
|
+
background-color: rgba(31, 41, 55, 0.8);
|
40
|
+
padding: 1rem;
|
41
|
+
border-radius: 0.5rem;
|
42
|
+
border: 1px solid rgb(55, 65, 81);
|
43
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
44
|
+
transition: border-color 0.3s;
|
45
|
+
text-align: center;
|
46
|
+
}
|
47
|
+
.summary-card:hover {
|
48
|
+
border-color: rgb(239, 68, 68);
|
49
|
+
}
|
50
|
+
.summary-card h3 {
|
51
|
+
font-size: 1.25rem;
|
52
|
+
margin-bottom: 0.5rem;
|
53
|
+
color: rgb(248, 113, 113);
|
54
|
+
}
|
55
|
+
.summary-card .value {
|
56
|
+
font-size: 2.25rem;
|
57
|
+
font-weight: 700;
|
58
|
+
margin-bottom: 0.5rem;
|
59
|
+
}
|
60
|
+
.summary-card .trend {
|
61
|
+
font-size: 0.875rem;
|
62
|
+
color: rgb(156, 163, 175);
|
63
|
+
}
|
64
|
+
.content-grid {
|
65
|
+
display: grid;
|
66
|
+
grid-template-columns: 1fr 1fr;
|
67
|
+
gap: 1.5rem;
|
68
|
+
margin-bottom: 2rem;
|
69
|
+
}
|
70
|
+
.section {
|
71
|
+
background-color: rgba(31, 41, 55, 0.8);
|
72
|
+
padding: 1rem;
|
73
|
+
border-radius: 0.5rem;
|
74
|
+
border: 1px solid rgb(55, 65, 81);
|
75
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
76
|
+
}
|
77
|
+
.section h3 {
|
78
|
+
font-size: 1.25rem;
|
79
|
+
margin-bottom: 1rem;
|
80
|
+
color: rgb(250, 204, 21);
|
81
|
+
text-align: center;
|
82
|
+
}
|
83
|
+
.section ul {
|
84
|
+
list-style-type: none;
|
85
|
+
padding: 0;
|
86
|
+
}
|
87
|
+
.section ul li {
|
88
|
+
border-bottom: 1px solid rgb(55, 65, 81);
|
89
|
+
padding: 0.5rem 0;
|
90
|
+
}
|
91
|
+
.section ul li:last-child {
|
92
|
+
border-bottom: none;
|
93
|
+
}
|
94
|
+
table {
|
95
|
+
width: 100%;
|
96
|
+
border-collapse: collapse;
|
97
|
+
font-size: 0.875rem;
|
98
|
+
}
|
99
|
+
table thead tr {
|
100
|
+
border-bottom: 1px solid rgb(55, 65, 81);
|
101
|
+
color: rgb(156, 163, 175);
|
102
|
+
}
|
103
|
+
table tbody tr {
|
104
|
+
border-bottom: 1px solid rgb(55, 65, 81);
|
105
|
+
transition: background-color 0.2s;
|
106
|
+
}
|
107
|
+
table tbody tr:hover {
|
108
|
+
background-color: rgba(127, 29, 29, 0.2);
|
109
|
+
}
|
110
|
+
table th, table td {
|
111
|
+
border: 1px solid rgb(55, 65, 81);
|
112
|
+
padding: 0.5rem 1rem;
|
113
|
+
text-align: left;
|
114
|
+
}
|
115
|
+
.footer {
|
116
|
+
margin-top: 2rem;
|
117
|
+
font-size: 0.75rem;
|
118
|
+
color: rgb(107, 114, 128);
|
119
|
+
text-align: center;
|
120
|
+
}
|
121
|
+
.footer a {
|
122
|
+
color: rgb(96, 165, 250);
|
123
|
+
text-decoration: none;
|
124
|
+
transition: color 0.2s;
|
125
|
+
}
|
126
|
+
.footer a:hover {
|
127
|
+
color: rgb(248, 113, 113);
|
128
|
+
}
|
129
|
+
.warning {
|
130
|
+
text-align: center;
|
131
|
+
color: #e74c3c;
|
132
|
+
margin: 30px 0;
|
133
|
+
font-size: 1.25rem;
|
134
|
+
}
|
135
|
+
.percentile-list li {
|
136
|
+
display: flex;
|
137
|
+
justify-content: space-between;
|
138
|
+
padding: 0.25rem 0;
|
139
|
+
}
|
140
|
+
</style>
|
141
|
+
</head>
|
142
|
+
<body>
|
143
|
+
<div class="container">
|
144
|
+
<div class="header">
|
145
|
+
<div>
|
146
|
+
<span class="title">🔍 Rspec Watchdog Dashboard</span>
|
147
|
+
<span class="version">v<%= Watchdog::Dashboard::VERSION %>-alpha</span>
|
148
|
+
</div>
|
149
|
+
<div class="last-scan">
|
150
|
+
Last scan: <span class="last-scan-time"><%= Time.current.strftime('%Y-%m-%d %H:%M:%S UTC') %></span>
|
151
|
+
</div>
|
152
|
+
</div>
|
153
|
+
|
154
|
+
<div class="summary-grid">
|
155
|
+
<div class="summary-card">
|
156
|
+
<h3>📊 Total Tests</h3>
|
157
|
+
<div class="value"><%= @total_tests %></div>
|
158
|
+
<div class="trend">All Tests</div>
|
159
|
+
</div>
|
160
|
+
<div class="summary-card">
|
161
|
+
<h3>✅ Passed Tests</h3>
|
162
|
+
<div class="value"><%= @passed_percentage %>%</div>
|
163
|
+
<div class="trend">Passing Rate</div>
|
164
|
+
</div>
|
165
|
+
<div class="summary-card">
|
166
|
+
<h3>🕒 Avg Execution</h3>
|
167
|
+
<div class="value"><%= sprintf('%.4f', @average_time) %>s</div>
|
168
|
+
<div class="trend">Test Duration</div>
|
169
|
+
</div>
|
170
|
+
<div class="summary-card">
|
171
|
+
<h3>❌ Failed Tests</h3>
|
172
|
+
<div class="value"><%= @failed_percentage %>%</div>
|
173
|
+
<div class="trend">Failure Rate</div>
|
174
|
+
</div>
|
175
|
+
</div>
|
176
|
+
|
177
|
+
<% if @failed_tests.any? %>
|
178
|
+
<div class="warning">❌ Failed Tests Detected ❌</div>
|
179
|
+
<% end %>
|
180
|
+
|
181
|
+
<div class="content-grid">
|
182
|
+
<div class="section">
|
183
|
+
<h3>⚡ Fastest vs 🐢 Slowest Test</h3>
|
184
|
+
<ul>
|
185
|
+
<li>
|
186
|
+
<strong>Fastest Test:</strong>
|
187
|
+
<%= @fastest_test.description %>
|
188
|
+
(Time: <%= sprintf('%.4f', @fastest_test.run_time) %> s)
|
189
|
+
</li>
|
190
|
+
<li>
|
191
|
+
<strong>Slowest Test:</strong>
|
192
|
+
<%= @slowest_test.description %>
|
193
|
+
(Time: <%= sprintf('%.4f', @slowest_test.run_time) %> s)
|
194
|
+
</li>
|
195
|
+
</ul>
|
196
|
+
</div>
|
197
|
+
<div class="section">
|
198
|
+
<h3>📊 Time Distribution</h3>
|
199
|
+
<ul>
|
200
|
+
<% @time_distribution_analysis.each do |category, count| %>
|
201
|
+
<li><%= category %>: <%= count %> tests</li>
|
202
|
+
<% end %>
|
203
|
+
</ul>
|
204
|
+
</div>
|
205
|
+
</div>
|
206
|
+
|
207
|
+
<div class="content-grid">
|
208
|
+
<div class="section percentile-list">
|
209
|
+
<h3>📈 Execution Time Percentiles</h3>
|
210
|
+
<ul>
|
211
|
+
<% @percentiles.each do |percentile| %>
|
212
|
+
<li>
|
213
|
+
<span><%= percentile[:percentile] %>% Percentile</span>
|
214
|
+
<span><%= sprintf('%.4f', percentile[:run_time]) %> s</span>
|
215
|
+
</li>
|
216
|
+
<% end %>
|
217
|
+
</ul>
|
218
|
+
</div>
|
219
|
+
<div class="section">
|
220
|
+
<h3>❌ Failed Tests Details</h3>
|
221
|
+
<ul>
|
222
|
+
<% @failed_tests.each do |test| %>
|
223
|
+
<li>
|
224
|
+
<%= test.description %>
|
225
|
+
(File: <%= test.file_path %>,
|
226
|
+
Location: <%= test.location %>)
|
227
|
+
</li>
|
228
|
+
<% end %>
|
229
|
+
</ul>
|
230
|
+
</div>
|
231
|
+
</div>
|
232
|
+
|
233
|
+
<div class="section">
|
234
|
+
<h3>📊 Detailed Test Metrics</h3>
|
235
|
+
<table>
|
236
|
+
<thead>
|
237
|
+
<tr>
|
238
|
+
<th>Description</th>
|
239
|
+
<th>File</th>
|
240
|
+
<th>Location</th>
|
241
|
+
<th>Execution Time (s)</th>
|
242
|
+
</tr>
|
243
|
+
</thead>
|
244
|
+
<tbody>
|
245
|
+
<% @metrics.each do |metric| %>
|
246
|
+
<tr>
|
247
|
+
<td><%= metric.description %></td>
|
248
|
+
<td><%= metric.file_path %></td>
|
249
|
+
<td><%= metric.location %></td>
|
250
|
+
<td><%= sprintf('%.4f', metric.run_time) %></td>
|
251
|
+
</tr>
|
252
|
+
<% end %>
|
253
|
+
</tbody>
|
254
|
+
</table>
|
255
|
+
</div>
|
256
|
+
|
257
|
+
<div class="footer">
|
258
|
+
<p>Dashboard <span class="version">v<%= Watchdog::Dashboard::VERSION %>-alpha</span> | Data refreshed every 6 hours |
|
259
|
+
<a href="#">Configure alerts</a> |
|
260
|
+
<a href="#">Export data</a>
|
261
|
+
</p>
|
262
|
+
</div>
|
263
|
+
</div>
|