rails_pulse 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/stylesheets/rails_pulse/components/tags.css +2 -2
- data/app/controllers/concerns/chart_table_concern.rb +4 -3
- data/app/controllers/rails_pulse/application_controller.rb +11 -3
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -8
- data/app/controllers/rails_pulse/queries_controller.rb +13 -8
- data/app/controllers/rails_pulse/requests_controller.rb +10 -5
- data/app/controllers/rails_pulse/routes_controller.rb +14 -7
- data/app/helpers/rails_pulse/application_helper.rb +47 -2
- data/app/helpers/rails_pulse/chart_helper.rb +32 -2
- data/app/javascript/rails_pulse/application.js +3 -54
- data/app/javascript/rails_pulse/controllers/chart_controller.js +229 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +9 -14
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +27 -33
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +0 -2
- data/app/jobs/rails_pulse/cleanup_job.rb +0 -2
- data/app/jobs/rails_pulse/summary_job.rb +0 -2
- 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 +6 -3
- data/app/models/rails_pulse/queries/tables/index.rb +12 -2
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +13 -7
- 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 +10 -6
- 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/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_page_header.html.erb +4 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_pagination.html.erb +8 -6
- data/app/views/rails_pulse/csp_test/show.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +8 -3
- data/app/views/rails_pulse/queries/index.html.erb +3 -2
- data/app/views/rails_pulse/queries/show.html.erb +2 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/index.html.erb +3 -2
- data/app/views/rails_pulse/routes/show.html.erb +2 -1
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +2 -2
- data/config/importmap.rb +1 -1
- data/lib/rails_pulse/cleanup_service.rb +8 -0
- data/lib/rails_pulse/engine.rb +0 -5
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/csp-test.js +10 -10
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +48 -48
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +7 -26
- data/app/models/concerns/taggable.rb +0 -61
- data/config/initializers/rails_charts_csp_patch.rb +0 -75
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
type: String, // "bar", "line", "area", "sparkline"
|
|
6
|
+
data: Object, // Chart data
|
|
7
|
+
options: Object, // ECharts configuration
|
|
8
|
+
theme: String // ECharts theme
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.initializeChart()
|
|
13
|
+
this.handleColorSchemeChange = this.onColorSchemeChange.bind(this)
|
|
14
|
+
document.addEventListener('rails-pulse:color-scheme-changed', this.handleColorSchemeChange)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
document.removeEventListener('rails-pulse:color-scheme-changed', this.handleColorSchemeChange)
|
|
19
|
+
this.disposeChart()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Main initialization with retry logic
|
|
23
|
+
initializeChart() {
|
|
24
|
+
this.retryCount = 0
|
|
25
|
+
this.maxRetries = 100 // 5 seconds
|
|
26
|
+
this.attemptInit()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
attemptInit() {
|
|
30
|
+
if (typeof echarts === 'undefined') {
|
|
31
|
+
this.retryCount++
|
|
32
|
+
if (this.retryCount >= this.maxRetries) {
|
|
33
|
+
console.error('[RailsPulse] echarts not loaded after 5 seconds for', this.element.id)
|
|
34
|
+
this.showError()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
setTimeout(() => this.attemptInit(), 50)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.renderChart()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
renderChart() {
|
|
45
|
+
try {
|
|
46
|
+
// Initialize chart
|
|
47
|
+
this.chart = echarts.init(this.element, this.themeValue || 'railspulse')
|
|
48
|
+
|
|
49
|
+
// Build and set options
|
|
50
|
+
const config = this.buildChartConfig()
|
|
51
|
+
this.chart.setOption(config)
|
|
52
|
+
|
|
53
|
+
// Apply current color scheme
|
|
54
|
+
this.applyColorScheme()
|
|
55
|
+
|
|
56
|
+
// Dispatch event for other controllers (event-based communication)
|
|
57
|
+
document.dispatchEvent(new CustomEvent('stimulus:echarts:rendered', {
|
|
58
|
+
detail: {
|
|
59
|
+
containerId: this.element.id,
|
|
60
|
+
chart: this.chart,
|
|
61
|
+
controller: this
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
// Responsive resize
|
|
66
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
67
|
+
if (this.chart) {
|
|
68
|
+
this.chart.resize()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
this.resizeObserver.observe(this.element)
|
|
72
|
+
|
|
73
|
+
// Mark as rendered for tests
|
|
74
|
+
this.element.setAttribute('data-chart-rendered', 'true')
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[RailsPulse] Error initializing chart:', error)
|
|
78
|
+
this.showError()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
buildChartConfig() {
|
|
83
|
+
// Start with provided options
|
|
84
|
+
const config = { ...this.optionsValue }
|
|
85
|
+
|
|
86
|
+
// Process formatters (convert function strings to actual functions)
|
|
87
|
+
this.processFormatters(config)
|
|
88
|
+
|
|
89
|
+
// Set data (xAxis and series)
|
|
90
|
+
this.setChartData(config)
|
|
91
|
+
|
|
92
|
+
return config
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setChartData(config) {
|
|
96
|
+
const data = this.dataValue
|
|
97
|
+
|
|
98
|
+
// Extract labels and values
|
|
99
|
+
const labels = Object.keys(data).map(k => {
|
|
100
|
+
const num = Number(k)
|
|
101
|
+
return isNaN(num) ? k : num
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const values = Object.values(data).map(v => {
|
|
105
|
+
if (typeof v === 'object' && v !== null) {
|
|
106
|
+
return v.value !== undefined ? v.value : v
|
|
107
|
+
}
|
|
108
|
+
return v
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Set xAxis data
|
|
112
|
+
config.xAxis = config.xAxis || {}
|
|
113
|
+
config.xAxis.type = 'category'
|
|
114
|
+
config.xAxis.data = labels
|
|
115
|
+
|
|
116
|
+
// Set yAxis
|
|
117
|
+
config.yAxis = config.yAxis || {}
|
|
118
|
+
config.yAxis.type = 'value'
|
|
119
|
+
|
|
120
|
+
// Set series data
|
|
121
|
+
if (Array.isArray(config.series)) {
|
|
122
|
+
// If series is already an array, update first series
|
|
123
|
+
config.series[0] = config.series[0] || {}
|
|
124
|
+
config.series[0].type = this.typeValue
|
|
125
|
+
config.series[0].data = values
|
|
126
|
+
} else if (config.series && typeof config.series === 'object') {
|
|
127
|
+
// If series is a single object (from helper), convert to array
|
|
128
|
+
const seriesConfig = { ...config.series }
|
|
129
|
+
config.series = [{
|
|
130
|
+
type: this.typeValue,
|
|
131
|
+
data: values,
|
|
132
|
+
...seriesConfig
|
|
133
|
+
}]
|
|
134
|
+
} else {
|
|
135
|
+
// No series provided, create default
|
|
136
|
+
config.series = [{
|
|
137
|
+
type: this.typeValue,
|
|
138
|
+
data: values
|
|
139
|
+
}]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
processFormatters(config) {
|
|
144
|
+
// Process tooltip formatter
|
|
145
|
+
if (config.tooltip?.formatter && typeof config.tooltip.formatter === 'string') {
|
|
146
|
+
config.tooltip.formatter = this.parseFormatter(config.tooltip.formatter)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Process xAxis formatter
|
|
150
|
+
if (config.xAxis?.axisLabel?.formatter && typeof config.xAxis.axisLabel.formatter === 'string') {
|
|
151
|
+
config.xAxis.axisLabel.formatter = this.parseFormatter(config.xAxis.axisLabel.formatter)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Process yAxis formatter
|
|
155
|
+
if (config.yAxis?.axisLabel?.formatter && typeof config.yAxis.axisLabel.formatter === 'string') {
|
|
156
|
+
config.yAxis.axisLabel.formatter = this.parseFormatter(config.yAxis.axisLabel.formatter)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parseFormatter(formatterString) {
|
|
161
|
+
// Remove function markers if present
|
|
162
|
+
const cleanString = formatterString.replace(/__FUNCTION_START__|__FUNCTION_END__/g, '')
|
|
163
|
+
|
|
164
|
+
// If it's a function string, parse it
|
|
165
|
+
if (cleanString.trim().startsWith('function')) {
|
|
166
|
+
try {
|
|
167
|
+
// eslint-disable-next-line no-eval
|
|
168
|
+
return eval(`(${cleanString})`)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('[RailsPulse] Error parsing formatter function:', error)
|
|
171
|
+
return cleanString
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return cleanString
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
showError() {
|
|
178
|
+
this.element.classList.add('chart-error')
|
|
179
|
+
this.element.innerHTML = '<p class="text-subtle p-4">Chart failed to load</p>'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Public accessor for chart instance
|
|
183
|
+
get chartInstance() {
|
|
184
|
+
return this.chart
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
disposeChart() {
|
|
188
|
+
if (this.resizeObserver) {
|
|
189
|
+
this.resizeObserver.disconnect()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.chart) {
|
|
193
|
+
this.chart.dispose()
|
|
194
|
+
this.chart = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Action for dynamic updates
|
|
199
|
+
update(event) {
|
|
200
|
+
if (event.detail?.data) {
|
|
201
|
+
this.dataValue = event.detail.data
|
|
202
|
+
}
|
|
203
|
+
if (event.detail?.options) {
|
|
204
|
+
this.optionsValue = event.detail.options
|
|
205
|
+
}
|
|
206
|
+
if (this.chart) {
|
|
207
|
+
const config = this.buildChartConfig()
|
|
208
|
+
this.chart.setOption(config, true) // true = not merge
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Color scheme management
|
|
213
|
+
onColorSchemeChange() {
|
|
214
|
+
this.applyColorScheme()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
applyColorScheme() {
|
|
218
|
+
if (!this.chart) return
|
|
219
|
+
|
|
220
|
+
const scheme = document.documentElement.getAttribute('data-color-scheme')
|
|
221
|
+
const isDark = scheme === 'dark'
|
|
222
|
+
const axisColor = isDark ? '#ffffff' : '#999999'
|
|
223
|
+
|
|
224
|
+
this.chart.setOption({
|
|
225
|
+
xAxis: { axisLabel: { color: axisColor } },
|
|
226
|
+
yAxis: { axisLabel: { color: axisColor } }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -15,21 +15,16 @@ export default class extends Controller {
|
|
|
15
15
|
originalSeriesOption = null;
|
|
16
16
|
|
|
17
17
|
connect() {
|
|
18
|
-
// Listen for the custom event '
|
|
19
|
-
// This event is
|
|
18
|
+
// Listen for the custom event 'stimulus:echarts:rendered' to set up the chart.
|
|
19
|
+
// This event is dispatched by the chart controller when the chart is ready.
|
|
20
20
|
this.handleChartInitialized = this.onChartInitialized.bind(this);
|
|
21
21
|
|
|
22
|
-
document.addEventListener('
|
|
23
|
-
|
|
24
|
-
// If the chart is already initialized (e.g., on back navigation), set up immediately
|
|
25
|
-
if (window.RailsCharts?.charts?.[this.chartIdValue]) {
|
|
26
|
-
this.setup();
|
|
27
|
-
}
|
|
22
|
+
document.addEventListener('stimulus:echarts:rendered', this.handleChartInitialized);
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
disconnect() {
|
|
31
|
-
// Remove the event listener
|
|
32
|
-
document.removeEventListener('
|
|
26
|
+
// Remove the event listener when the controller is disconnected
|
|
27
|
+
document.removeEventListener('stimulus:echarts:rendered', this.handleChartInitialized);
|
|
33
28
|
|
|
34
29
|
// Remove chart event listeners if they exist
|
|
35
30
|
if (this.hasChartTarget && this.chartTarget) {
|
|
@@ -47,6 +42,8 @@ export default class extends Controller {
|
|
|
47
42
|
// After the chart is initialized, set up the event listeners and data tracking
|
|
48
43
|
onChartInitialized(event) {
|
|
49
44
|
if (event.detail.containerId === this.chartIdValue) {
|
|
45
|
+
// Store the chart instance from the event
|
|
46
|
+
this.chart = event.detail.chart;
|
|
50
47
|
this.setup();
|
|
51
48
|
}
|
|
52
49
|
}
|
|
@@ -56,7 +53,7 @@ export default class extends Controller {
|
|
|
56
53
|
return; // Prevent multiple setups
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
// We need both the chart target in DOM and the chart object from
|
|
56
|
+
// We need both the chart target in DOM and the chart object from the event
|
|
60
57
|
let hasTarget = false;
|
|
61
58
|
try {
|
|
62
59
|
hasTarget = !!this.chartTarget;
|
|
@@ -64,10 +61,8 @@ export default class extends Controller {
|
|
|
64
61
|
hasTarget = false;
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
// Get the chart element which the RailsCharts library has created
|
|
68
|
-
this.chart = window.RailsCharts.charts[this.chartIdValue];
|
|
69
|
-
|
|
70
64
|
// Only proceed if we have BOTH the DOM target and the chart object
|
|
65
|
+
// (chart is set by onChartInitialized from the event)
|
|
71
66
|
if (!hasTarget || !this.chart) {
|
|
72
67
|
return;
|
|
73
68
|
}
|
|
@@ -3,56 +3,50 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static targets = ["limit"]
|
|
5
5
|
static values = {
|
|
6
|
-
storageKey: { type: String, default: "rails_pulse_pagination_limit" }
|
|
7
|
-
url: String
|
|
6
|
+
storageKey: { type: String, default: "rails_pulse_pagination_limit" }
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
connect() {
|
|
11
10
|
this.restorePaginationLimit()
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
// Update pagination limit and
|
|
13
|
+
// Update pagination limit and navigate to page 1 with new limit
|
|
15
14
|
updateLimit() {
|
|
16
15
|
const limit = this.limitTarget.value
|
|
17
16
|
|
|
18
|
-
// Save to session storage
|
|
17
|
+
// Save to session storage for persistence
|
|
19
18
|
sessionStorage.setItem(this.storageKeyValue, limit)
|
|
20
19
|
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
// Build URL with limit parameter and reset to page 1
|
|
21
|
+
const currentUrl = new URL(window.location)
|
|
22
|
+
currentUrl.searchParams.set('limit', limit)
|
|
23
|
+
currentUrl.searchParams.delete('page')
|
|
24
|
+
|
|
25
|
+
// Use Turbo.visit for smooth navigation that preserves query params
|
|
26
|
+
if (typeof Turbo !== 'undefined') {
|
|
27
|
+
Turbo.visit(currentUrl.toString(), { action: 'replace' })
|
|
28
28
|
} else {
|
|
29
|
-
|
|
30
|
-
const currentUrl = new URL(window.location)
|
|
31
|
-
currentUrl.searchParams.set('limit', limit)
|
|
32
|
-
window.location.href = currentUrl.pathname + currentUrl.search
|
|
29
|
+
window.location.href = currentUrl.toString()
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
//
|
|
37
|
-
getCSRFToken() {
|
|
38
|
-
const token = document.querySelector('meta[name="csrf-token"]')
|
|
39
|
-
return token ? token.getAttribute('content') : ''
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Save the pagination limit to session storage when it changes
|
|
43
|
-
savePaginationLimit() {
|
|
44
|
-
const limit = this.limitTarget.value
|
|
45
|
-
sessionStorage.setItem(this.storageKeyValue, limit)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Restore the pagination limit from session storage on page load
|
|
33
|
+
// Restore the pagination limit from URL or session storage on page load
|
|
49
34
|
restorePaginationLimit() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
// URL params take precedence over session storage
|
|
36
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
37
|
+
const urlLimit = urlParams.get('limit')
|
|
38
|
+
|
|
39
|
+
if (urlLimit && this.limitTarget) {
|
|
40
|
+
// Sync sessionStorage with URL param
|
|
41
|
+
sessionStorage.setItem(this.storageKeyValue, urlLimit)
|
|
42
|
+
if (this.limitTarget.value !== urlLimit) {
|
|
43
|
+
this.limitTarget.value = urlLimit
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
// Fall back to sessionStorage if no URL param
|
|
47
|
+
const savedLimit = sessionStorage.getItem(this.storageKeyValue)
|
|
48
|
+
if (savedLimit && this.limitTarget && this.limitTarget.value !== savedLimit) {
|
|
54
49
|
this.limitTarget.value = savedLimit
|
|
55
|
-
// Don't trigger change event when restoring from session - prevents infinite loops
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Taggable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
# Callbacks
|
|
7
|
+
before_save :ensure_tags_is_array
|
|
8
|
+
|
|
9
|
+
# Scopes with table name qualification to avoid ambiguity
|
|
10
|
+
scope :with_tag, ->(tag) { where("#{table_name}.tags LIKE ?", "%#{tag}%") }
|
|
11
|
+
scope :without_tag, ->(tag) { where.not("#{table_name}.tags LIKE ?", "%#{tag}%") }
|
|
12
|
+
scope :with_tags, -> { where("#{table_name}.tags IS NOT NULL AND #{table_name}.tags != '[]'") }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Tag management methods
|
|
16
|
+
def tag_list
|
|
17
|
+
parsed_tags || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tag_list=(value)
|
|
21
|
+
self.tags = value.to_json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def has_tag?(tag)
|
|
25
|
+
tag_list.include?(tag.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_tag(tag)
|
|
29
|
+
current_tags = tag_list
|
|
30
|
+
unless current_tags.include?(tag.to_s)
|
|
31
|
+
current_tags << tag.to_s
|
|
32
|
+
self.tag_list = current_tags
|
|
33
|
+
save
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def remove_tag(tag)
|
|
38
|
+
current_tags = tag_list
|
|
39
|
+
if current_tags.include?(tag.to_s)
|
|
40
|
+
current_tags.delete(tag.to_s)
|
|
41
|
+
self.tag_list = current_tags
|
|
42
|
+
save
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def parsed_tags
|
|
49
|
+
return [] if tags.nil? || tags.empty?
|
|
50
|
+
JSON.parse(tags)
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
[]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ensure_tags_is_array
|
|
56
|
+
if tags.nil?
|
|
57
|
+
self.tags = "[]"
|
|
58
|
+
elsif tags.is_a?(Array)
|
|
59
|
+
self.tags = tags.to_json
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -2,6 +2,11 @@ module RailsPulse
|
|
|
2
2
|
module Dashboard
|
|
3
3
|
module Charts
|
|
4
4
|
class AverageResponseTime
|
|
5
|
+
def initialize(disabled_tags: [], show_non_tagged: true)
|
|
6
|
+
@disabled_tags = disabled_tags
|
|
7
|
+
@show_non_tagged = show_non_tagged
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def to_chart_data
|
|
6
11
|
# Create a range of all dates in the past 2 weeks
|
|
7
12
|
start_date = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -9,11 +14,13 @@ module RailsPulse
|
|
|
9
14
|
date_range = (start_date..end_date)
|
|
10
15
|
|
|
11
16
|
# Get the actual data from Summary records (routes)
|
|
12
|
-
summaries = RailsPulse::Summary
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
summaries = RailsPulse::Summary
|
|
18
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
19
|
+
.where(
|
|
20
|
+
summarizable_type: "RailsPulse::Route",
|
|
21
|
+
period_type: "day",
|
|
22
|
+
period_start: start_date.beginning_of_day..end_date.end_of_day
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
# Group by day manually for cross-database compatibility
|
|
19
26
|
actual_data = {}
|
|
@@ -2,6 +2,11 @@ module RailsPulse
|
|
|
2
2
|
module Dashboard
|
|
3
3
|
module Charts
|
|
4
4
|
class P95ResponseTime
|
|
5
|
+
def initialize(disabled_tags: [], show_non_tagged: true)
|
|
6
|
+
@disabled_tags = disabled_tags
|
|
7
|
+
@show_non_tagged = show_non_tagged
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def to_chart_data
|
|
6
11
|
# Create a range of all dates in the past 2 weeks
|
|
7
12
|
start_date = 2.weeks.ago.beginning_of_day.to_date
|
|
@@ -9,11 +14,13 @@ module RailsPulse
|
|
|
9
14
|
date_range = (start_date..end_date)
|
|
10
15
|
|
|
11
16
|
# Get the actual data from Summary records (queries for P95)
|
|
12
|
-
summaries = RailsPulse::Summary
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
summaries = RailsPulse::Summary
|
|
18
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
19
|
+
.where(
|
|
20
|
+
summarizable_type: "RailsPulse::Query",
|
|
21
|
+
period_type: "day",
|
|
22
|
+
period_start: start_date.beginning_of_day..end_date.end_of_day
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
actual_data = summaries
|
|
19
26
|
.group_by_day(:period_start, time_zone: Time.zone)
|
|
@@ -3,6 +3,12 @@ module RailsPulse
|
|
|
3
3
|
module Tables
|
|
4
4
|
class SlowQueries
|
|
5
5
|
include RailsPulse::FormattingHelper
|
|
6
|
+
|
|
7
|
+
def initialize(disabled_tags: [], show_non_tagged: true)
|
|
8
|
+
@disabled_tags = disabled_tags
|
|
9
|
+
@show_non_tagged = show_non_tagged
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
def to_table_data
|
|
7
13
|
# Get data for this week
|
|
8
14
|
this_week_start = 1.week.ago.beginning_of_week
|
|
@@ -10,6 +16,7 @@ module RailsPulse
|
|
|
10
16
|
|
|
11
17
|
# Fetch query data from Summary records for this week
|
|
12
18
|
query_data = RailsPulse::Summary
|
|
19
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
13
20
|
.joins("INNER JOIN rails_pulse_queries ON rails_pulse_queries.id = rails_pulse_summaries.summarizable_id")
|
|
14
21
|
.where(
|
|
15
22
|
summarizable_type: "RailsPulse::Query",
|
|
@@ -4,6 +4,11 @@ module RailsPulse
|
|
|
4
4
|
class SlowRoutes
|
|
5
5
|
include RailsPulse::FormattingHelper
|
|
6
6
|
|
|
7
|
+
def initialize(disabled_tags: [], show_non_tagged: true)
|
|
8
|
+
@disabled_tags = disabled_tags
|
|
9
|
+
@show_non_tagged = show_non_tagged
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def to_table_data
|
|
8
13
|
# Get data for this week
|
|
9
14
|
this_week_start = 1.week.ago.beginning_of_week
|
|
@@ -11,6 +16,7 @@ module RailsPulse
|
|
|
11
16
|
|
|
12
17
|
# Fetch route data from Summary records for this week
|
|
13
18
|
route_data = RailsPulse::Summary
|
|
19
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
14
20
|
.joins("INNER JOIN rails_pulse_routes ON rails_pulse_routes.id = rails_pulse_summaries.summarizable_id")
|
|
15
21
|
.where(
|
|
16
22
|
summarizable_type: "RailsPulse::Route",
|
|
@@ -2,8 +2,10 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class AverageQueryTimes
|
|
5
|
-
def initialize(query: nil)
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@query = query
|
|
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 aggregated metrics with conditional sums
|
|
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::Query",
|
|
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: @query.id) if @query
|
|
20
24
|
|
|
21
25
|
metrics = base_query.select(
|
|
@@ -2,8 +2,10 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class ExecutionRate
|
|
5
|
-
def initialize(query: nil)
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@query = query
|
|
7
|
+
@disabled_tags = disabled_tags
|
|
8
|
+
@show_non_tagged = show_non_tagged
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
def to_metric_card
|
|
@@ -12,20 +14,24 @@ module RailsPulse
|
|
|
12
14
|
|
|
13
15
|
# Get the most common period type for this query, or fall back to "day"
|
|
14
16
|
period_type = if @query
|
|
15
|
-
RailsPulse::Summary
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
RailsPulse::Summary
|
|
18
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
19
|
+
.where(
|
|
20
|
+
summarizable_type: "RailsPulse::Query",
|
|
21
|
+
summarizable_id: @query.id
|
|
22
|
+
).group(:period_type).count.max_by(&:last)&.first || "day"
|
|
19
23
|
else
|
|
20
24
|
"day"
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# Single query to get all count metrics with conditional aggregation
|
|
24
|
-
base_query = RailsPulse::Summary
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
base_query = RailsPulse::Summary
|
|
29
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
30
|
+
.where(
|
|
31
|
+
summarizable_type: "RailsPulse::Query",
|
|
32
|
+
period_type: period_type,
|
|
33
|
+
period_start: 2.weeks.ago.beginning_of_day..Time.current
|
|
34
|
+
)
|
|
29
35
|
base_query = base_query.where(summarizable_id: @query.id) if @query
|
|
30
36
|
|
|
31
37
|
metrics = base_query.select(
|
|
@@ -2,8 +2,10 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Cards
|
|
4
4
|
class PercentileQueryTimes
|
|
5
|
-
def initialize(query: nil)
|
|
5
|
+
def initialize(query: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@query = query
|
|
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 P95 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::Query",
|
|
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: @query.id) if @query
|
|
20
24
|
|
|
21
25
|
metrics = base_query.select(
|