rails_pulse 0.1.4 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +78 -0
- data/Rakefile +152 -3
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
- data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
- data/app/controllers/concerns/chart_table_concern.rb +2 -0
- data/app/controllers/concerns/response_range_concern.rb +15 -2
- data/app/controllers/concerns/tag_filter_concern.rb +26 -0
- data/app/controllers/concerns/time_range_concern.rb +27 -8
- data/app/controllers/rails_pulse/application_controller.rb +74 -0
- data/app/controllers/rails_pulse/dashboard_controller.rb +12 -8
- data/app/controllers/rails_pulse/queries_controller.rb +15 -7
- data/app/controllers/rails_pulse/requests_controller.rb +48 -12
- data/app/controllers/rails_pulse/routes_controller.rb +14 -5
- data/app/controllers/rails_pulse/tags_controller.rb +51 -0
- data/app/helpers/rails_pulse/application_helper.rb +2 -0
- data/app/helpers/rails_pulse/form_helper.rb +75 -0
- data/app/helpers/rails_pulse/tags_helper.rb +29 -0
- data/app/javascript/rails_pulse/application.js +6 -0
- data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
- data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
- data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
- data/app/models/concerns/rails_pulse/taggable.rb +63 -0
- data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +12 -5
- data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +12 -5
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +7 -0
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +6 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +10 -6
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +16 -10
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +10 -6
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +5 -2
- data/app/models/rails_pulse/queries/tables/index.rb +20 -2
- data/app/models/rails_pulse/query.rb +2 -0
- data/app/models/rails_pulse/request.rb +9 -1
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +12 -6
- data/app/models/rails_pulse/route.rb +2 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +10 -6
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +10 -6
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +10 -6
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +10 -6
- data/app/models/rails_pulse/routes/charts/average_response_times.rb +9 -5
- data/app/models/rails_pulse/routes/tables/index.rb +20 -2
- data/app/models/rails_pulse/summary.rb +55 -0
- data/app/services/rails_pulse/summary_service.rb +2 -0
- data/app/views/layouts/rails_pulse/_global_filters.html.erb +91 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +8 -5
- data/app/views/rails_pulse/components/_active_filters.html.erb +36 -0
- data/app/views/rails_pulse/components/_page_header.html.erb +24 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +4 -0
- data/app/views/rails_pulse/operations/show.html.erb +1 -1
- data/app/views/rails_pulse/queries/_table.html.erb +3 -1
- data/app/views/rails_pulse/queries/index.html.erb +3 -7
- data/app/views/rails_pulse/queries/show.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +3 -1
- data/app/views/rails_pulse/requests/index.html.erb +44 -62
- data/app/views/rails_pulse/requests/show.html.erb +1 -1
- data/app/views/rails_pulse/routes/_requests_table.html.erb +3 -1
- data/app/views/rails_pulse/routes/_table.html.erb +3 -1
- data/app/views/rails_pulse/routes/index.html.erb +4 -8
- data/app/views/rails_pulse/routes/show.html.erb +3 -7
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
- data/config/initializers/rails_charts_csp_patch.rb +9 -9
- data/config/routes.rb +5 -0
- data/db/rails_pulse_schema.rb +3 -0
- data/lib/generators/rails_pulse/install_generator.rb +21 -2
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +3 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +145 -29
- data/lib/rails_pulse/cleanup_service.rb +8 -0
- data/lib/rails_pulse/configuration.rb +16 -1
- data/lib/rails_pulse/engine.rb +25 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- 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 +73 -69
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +18 -3
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["selectWrapper", "pickerWrapper"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
const selectElement = this.selectWrapperTarget.querySelector('select')
|
|
8
|
+
if (!selectElement) return
|
|
9
|
+
|
|
10
|
+
// Check if we're in recent_custom mode
|
|
11
|
+
const mode = selectElement.dataset.mode
|
|
12
|
+
|
|
13
|
+
if (mode === 'recent_custom') {
|
|
14
|
+
// In recent_custom mode, "recent" shows no picker, "custom" shows picker
|
|
15
|
+
if (selectElement.value === "custom") {
|
|
16
|
+
this.showPicker()
|
|
17
|
+
this.initializeDatePicker()
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
// In preset mode, "custom" shows picker
|
|
21
|
+
if (selectElement.value === "custom") {
|
|
22
|
+
this.showPicker()
|
|
23
|
+
this.initializeDatePicker()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// When time range is selected from dropdown
|
|
29
|
+
handleChange(event) {
|
|
30
|
+
const mode = event.target.dataset.mode
|
|
31
|
+
|
|
32
|
+
if (mode === 'recent_custom') {
|
|
33
|
+
// In recent_custom mode
|
|
34
|
+
if (event.target.value === "custom") {
|
|
35
|
+
this.showPicker()
|
|
36
|
+
this.openDatePicker()
|
|
37
|
+
} else {
|
|
38
|
+
// "recent" is selected - hide picker
|
|
39
|
+
this.pickerWrapperTarget.style.display = "none"
|
|
40
|
+
this.selectWrapperTarget.style.display = "block"
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// In preset mode
|
|
44
|
+
if (event.target.value === "custom") {
|
|
45
|
+
this.showPicker()
|
|
46
|
+
this.openDatePicker()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Show picker, hide select
|
|
52
|
+
showPicker() {
|
|
53
|
+
this.selectWrapperTarget.style.display = "none"
|
|
54
|
+
this.pickerWrapperTarget.style.display = "flex"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Open the flatpickr calendar
|
|
58
|
+
openDatePicker() {
|
|
59
|
+
// Wait a bit for the DOM to update and flatpickr to initialize
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
// Find the original hidden input that has the datepicker controller
|
|
62
|
+
const hiddenInput = this.pickerWrapperTarget.querySelector('input[name*="custom_date_range"]')
|
|
63
|
+
if (!hiddenInput) return
|
|
64
|
+
|
|
65
|
+
// Get the datepicker controller from the hidden input
|
|
66
|
+
const datepickerController = this.application.getControllerForElementAndIdentifier(
|
|
67
|
+
hiddenInput,
|
|
68
|
+
'rails-pulse--datepicker'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (datepickerController && datepickerController.flatpickr) {
|
|
72
|
+
datepickerController.flatpickr.open()
|
|
73
|
+
}
|
|
74
|
+
}, 50)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Show select, hide picker
|
|
78
|
+
showSelect() {
|
|
79
|
+
this.pickerWrapperTarget.style.display = "none"
|
|
80
|
+
this.selectWrapperTarget.style.display = "block"
|
|
81
|
+
|
|
82
|
+
// Reset select to default value based on mode
|
|
83
|
+
const selectElement = this.selectWrapperTarget.querySelector('select')
|
|
84
|
+
if (selectElement) {
|
|
85
|
+
const mode = selectElement.dataset.mode
|
|
86
|
+
if (mode === 'recent_custom') {
|
|
87
|
+
selectElement.value = "recent"
|
|
88
|
+
} else {
|
|
89
|
+
selectElement.value = "last_day"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Initialize flatpickr with existing date value
|
|
95
|
+
initializeDatePicker() {
|
|
96
|
+
const dateInput = this.pickerWrapperTarget.querySelector('input[type="text"]')
|
|
97
|
+
if (!dateInput || !dateInput.value) return
|
|
98
|
+
|
|
99
|
+
// Get the datepicker controller
|
|
100
|
+
const datepickerController = this.application.getControllerForElementAndIdentifier(
|
|
101
|
+
dateInput,
|
|
102
|
+
'rails-pulse--datepicker'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (datepickerController && datepickerController.flatpickr) {
|
|
106
|
+
const value = dateInput.value
|
|
107
|
+
// Parse the "start to end" format
|
|
108
|
+
if (value.includes(' to ')) {
|
|
109
|
+
const [start, end] = value.split(' to ').map(d => d.trim())
|
|
110
|
+
// Set the dates in flatpickr
|
|
111
|
+
datepickerController.flatpickr.setDate([start, end], false)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import flatpickr from "flatpickr"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = [ "details" ]
|
|
6
|
+
static values = {
|
|
7
|
+
type: String, disable: Array,
|
|
8
|
+
mode: { type: String, default: "single" },
|
|
9
|
+
showMonths: { type: Number, default: 1 },
|
|
10
|
+
dateFormat: { type: String, default: "F d, Y" },
|
|
11
|
+
dateTimeFormat: { type: String, default: "M d, Y h:i K" }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
if (this.typeValue == "time") {
|
|
16
|
+
this.flatpickr = flatpickr(this.element, this.#timeOptions)
|
|
17
|
+
} else if (this.typeValue == "datetime") {
|
|
18
|
+
this.flatpickr = flatpickr(this.element, this.#dateTimeOptions)
|
|
19
|
+
} else {
|
|
20
|
+
this.flatpickr = flatpickr(this.element, this.#basicOptions)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnect() {
|
|
25
|
+
this.flatpickr.destroy()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get #timeOptions() {
|
|
29
|
+
return { dateFormat: "H:i", enableTime: true, noCalendar: true }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get #dateTimeOptions() {
|
|
33
|
+
return { ...this.#baseOptions, altFormat: this.dateTimeFormatValue, dateFormat: "Y-m-d H:i", enableTime: true }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get #basicOptions() {
|
|
37
|
+
return { ...this.#baseOptions, altFormat: this.dateFormatValue, dateFormat: "Y-m-d" }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get #baseOptions() {
|
|
41
|
+
return {
|
|
42
|
+
altInput: true,
|
|
43
|
+
disable: this.disableValue,
|
|
44
|
+
mode: this.modeValue,
|
|
45
|
+
showMonths: this.showMonthsValue
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["wrapper", "dialog", "dateRange", "indicator", "form"]
|
|
5
|
+
static values = {
|
|
6
|
+
active: { type: Boolean, default: false }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.updateIndicator()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Open the global filters dialog
|
|
14
|
+
open(event) {
|
|
15
|
+
event.preventDefault()
|
|
16
|
+
|
|
17
|
+
// If there's a value in the date range input, make sure flatpickr knows about it
|
|
18
|
+
if (this.dateRangeTarget.value) {
|
|
19
|
+
const datepickerController = this.application.getControllerForElementAndIdentifier(
|
|
20
|
+
this.dateRangeTarget,
|
|
21
|
+
'rails-pulse--datepicker'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if (datepickerController && datepickerController.flatpickr) {
|
|
25
|
+
const value = this.dateRangeTarget.value
|
|
26
|
+
// Parse the "start to end" format
|
|
27
|
+
if (value.includes(' to ')) {
|
|
28
|
+
const [start, end] = value.split(' to ').map(d => d.trim())
|
|
29
|
+
// Set the dates in flatpickr
|
|
30
|
+
datepickerController.flatpickr.setDate([start, end], false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.wrapperTarget.style.display = 'flex'
|
|
36
|
+
// Prevent body scroll when dialog is open
|
|
37
|
+
document.body.style.overflow = 'hidden'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Close the dialog
|
|
41
|
+
close(event) {
|
|
42
|
+
if (event) {
|
|
43
|
+
event.preventDefault()
|
|
44
|
+
}
|
|
45
|
+
this.wrapperTarget.style.display = 'none'
|
|
46
|
+
// Restore body scroll
|
|
47
|
+
document.body.style.overflow = ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Close dialog when clicking outside
|
|
51
|
+
closeOnClickOutside(event) {
|
|
52
|
+
if (event.target === this.wrapperTarget) {
|
|
53
|
+
this.close(event)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle form submission - parse date range and add individual params
|
|
58
|
+
submit(event) {
|
|
59
|
+
// If clear button was clicked, let it through as-is
|
|
60
|
+
if (event.submitter && event.submitter.name === "clear") {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dateRangeValue = this.dateRangeTarget.value
|
|
65
|
+
const form = event.target
|
|
66
|
+
|
|
67
|
+
// Parse date range if provided
|
|
68
|
+
if (dateRangeValue && dateRangeValue.includes(' to ')) {
|
|
69
|
+
const [startTime, endTime] = dateRangeValue.split(' to ').map(d => d.trim())
|
|
70
|
+
|
|
71
|
+
// Remove any existing hidden inputs
|
|
72
|
+
form.querySelectorAll('input[name="start_time"], input[name="end_time"]').forEach(el => el.remove())
|
|
73
|
+
|
|
74
|
+
// Add new hidden inputs
|
|
75
|
+
const startInput = document.createElement('input')
|
|
76
|
+
startInput.type = 'hidden'
|
|
77
|
+
startInput.name = 'start_time'
|
|
78
|
+
startInput.value = startTime
|
|
79
|
+
form.appendChild(startInput)
|
|
80
|
+
|
|
81
|
+
const endInput = document.createElement('input')
|
|
82
|
+
endInput.type = 'hidden'
|
|
83
|
+
endInput.name = 'end_time'
|
|
84
|
+
endInput.value = endTime
|
|
85
|
+
form.appendChild(endInput)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tag switches are already being submitted as enabled_tags[]
|
|
89
|
+
// The controller will convert these to disabled_tags
|
|
90
|
+
// No additional processing needed here
|
|
91
|
+
|
|
92
|
+
// No validation needed - user can apply any combination of filters
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Update visual indicator based on activeValue
|
|
96
|
+
updateIndicator() {
|
|
97
|
+
if (this.hasIndicatorTarget) {
|
|
98
|
+
if (this.activeValue) {
|
|
99
|
+
this.indicatorTarget.classList.add("global-filters-active")
|
|
100
|
+
} else {
|
|
101
|
+
this.indicatorTarget.classList.remove("global-filters-active")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Called when activeValue changes
|
|
107
|
+
activeValueChanged() {
|
|
108
|
+
this.updateIndicator()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -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(
|
|
@@ -2,18 +2,21 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Charts
|
|
4
4
|
class AverageQueryTimes
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, query: nil, start_time: nil, end_time: nil, start_duration: nil)
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, query: nil, start_time: nil, end_time: nil, start_duration: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@query = query
|
|
9
9
|
@start_time = start_time
|
|
10
10
|
@end_time = end_time
|
|
11
11
|
@start_duration = start_duration
|
|
12
|
+
@disabled_tags = disabled_tags
|
|
13
|
+
@show_non_tagged = show_non_tagged
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def to_rails_chart
|
|
15
|
-
# The ransack query already contains the correct filters, just add period_type
|
|
17
|
+
# The ransack query already contains the correct filters, just add period_type and tag filters
|
|
16
18
|
summaries = @ransack_query.result(distinct: false)
|
|
19
|
+
.with_tag_filters(@disabled_tags, @show_non_tagged)
|
|
17
20
|
.where(period_type: @period_type)
|
|
18
21
|
.group(:period_start)
|
|
19
22
|
.having("AVG(avg_duration) > ?", @start_duration || 0)
|
|
@@ -2,12 +2,14 @@ module RailsPulse
|
|
|
2
2
|
module Queries
|
|
3
3
|
module Tables
|
|
4
4
|
class Index
|
|
5
|
-
def initialize(ransack_query:, period_type: nil, start_time:, params:, query: nil)
|
|
5
|
+
def initialize(ransack_query:, period_type: nil, start_time:, params:, query: nil, disabled_tags: [], show_non_tagged: true)
|
|
6
6
|
@ransack_query = ransack_query
|
|
7
7
|
@period_type = period_type
|
|
8
8
|
@start_time = start_time
|
|
9
9
|
@params = params
|
|
10
10
|
@query = query
|
|
11
|
+
@disabled_tags = disabled_tags
|
|
12
|
+
@show_non_tagged = show_non_tagged
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_table
|
|
@@ -21,6 +23,20 @@ module RailsPulse
|
|
|
21
23
|
period_type: @period_type
|
|
22
24
|
)
|
|
23
25
|
|
|
26
|
+
# Apply tag filters by excluding queries with disabled tags
|
|
27
|
+
# Separate "non_tagged" from actual tags (it's a virtual tag)
|
|
28
|
+
actual_disabled_tags = @disabled_tags.reject { |tag| tag == "non_tagged" }
|
|
29
|
+
|
|
30
|
+
# Exclude queries with actual disabled tags
|
|
31
|
+
actual_disabled_tags.each do |tag|
|
|
32
|
+
base_query = base_query.where.not("rails_pulse_queries.tags LIKE ?", "%#{tag}%")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Exclude non-tagged queries if show_non_tagged is false
|
|
36
|
+
unless @show_non_tagged
|
|
37
|
+
base_query = base_query.where("rails_pulse_queries.tags IS NOT NULL AND rails_pulse_queries.tags != '[]'")
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
base_query = base_query.where(summarizable_id: @query.id) if @query
|
|
25
41
|
|
|
26
42
|
# Apply grouping and aggregation
|
|
@@ -29,13 +45,15 @@ module RailsPulse
|
|
|
29
45
|
"rails_pulse_summaries.summarizable_id",
|
|
30
46
|
"rails_pulse_summaries.summarizable_type",
|
|
31
47
|
"rails_pulse_queries.id",
|
|
32
|
-
"rails_pulse_queries.normalized_sql"
|
|
48
|
+
"rails_pulse_queries.normalized_sql",
|
|
49
|
+
"rails_pulse_queries.tags"
|
|
33
50
|
)
|
|
34
51
|
.select(
|
|
35
52
|
"rails_pulse_summaries.summarizable_id",
|
|
36
53
|
"rails_pulse_summaries.summarizable_type",
|
|
37
54
|
"rails_pulse_queries.id as query_id",
|
|
38
55
|
"rails_pulse_queries.normalized_sql",
|
|
56
|
+
"rails_pulse_queries.tags",
|
|
39
57
|
"AVG(rails_pulse_summaries.avg_duration) as avg_duration",
|
|
40
58
|
"MAX(rails_pulse_summaries.max_duration) as max_duration",
|
|
41
59
|
"SUM(rails_pulse_summaries.count) as execution_count",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
class Request < RailsPulse::ApplicationRecord
|
|
3
|
+
include Taggable
|
|
4
|
+
|
|
3
5
|
self.table_name = "rails_pulse_requests"
|
|
4
6
|
|
|
5
7
|
# Associations
|
|
@@ -17,7 +19,7 @@ module RailsPulse
|
|
|
17
19
|
before_create :set_request_uuid
|
|
18
20
|
|
|
19
21
|
def self.ransackable_attributes(auth_object = nil)
|
|
20
|
-
%w[id route_id occurred_at duration status status_indicator route_path]
|
|
22
|
+
%w[id route_id occurred_at duration status status_category status_indicator route_path]
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def self.ransackable_associations(auth_object = nil)
|
|
@@ -32,6 +34,12 @@ module RailsPulse
|
|
|
32
34
|
Arel.sql("rails_pulse_routes.path")
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
ransacker :status_category do |parent|
|
|
38
|
+
# Returns the first digit of the status code (2, 3, 4, or 5)
|
|
39
|
+
# Use FLOOR instead of CAST for cross-database compatibility
|
|
40
|
+
Arel.sql("FLOOR(#{parent.table[:status].name} / 100)")
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
ransacker :status_indicator do |parent|
|
|
36
44
|
# Calculate status indicator based on request_thresholds with safe defaults
|
|
37
45
|
config = RailsPulse.configuration rescue nil
|