rails_pulse 0.3.3 → 0.4.0.pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -0
  3. data/README.md +44 -3
  4. data/app/assets/stylesheets/rails_pulse/application.css +21 -32
  5. data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
  6. data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
  7. data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
  8. data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
  9. data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
  10. data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
  11. data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
  12. data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
  13. data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
  14. data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
  15. data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
  16. data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
  17. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
  18. data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
  19. data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
  20. data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
  21. data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
  22. data/app/controllers/concerns/chart_table_concern.rb +15 -3
  23. data/app/controllers/concerns/ransack_params_concern.rb +18 -0
  24. data/app/controllers/concerns/response_range_concern.rb +1 -1
  25. data/app/controllers/concerns/time_range_concern.rb +28 -14
  26. data/app/controllers/rails_pulse/application_controller.rb +104 -31
  27. data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
  28. data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
  29. data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
  30. data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
  31. data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
  32. data/app/controllers/rails_pulse/requests_controller.rb +1 -1
  33. data/app/controllers/rails_pulse/routes_controller.rb +7 -1
  34. data/app/controllers/rails_pulse/storage_controller.rb +7 -0
  35. data/app/controllers/rails_pulse/tags_controller.rb +2 -2
  36. data/app/helpers/rails_pulse/application_helper.rb +1 -0
  37. data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
  38. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
  39. data/app/helpers/rails_pulse/chart_helper.rb +0 -3
  40. data/app/helpers/rails_pulse/route_helper.rb +8 -16
  41. data/app/javascript/rails_pulse/application.js +29 -8
  42. data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
  43. data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
  44. data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
  45. data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
  46. data/app/models/concerns/rails_pulse/taggable.rb +11 -6
  47. data/app/models/rails_pulse/cards/base.rb +53 -0
  48. data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
  49. data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
  50. data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
  51. data/app/models/rails_pulse/deployment.rb +47 -1
  52. data/app/models/rails_pulse/exception_group.rb +37 -0
  53. data/app/models/rails_pulse/exception_occurrence.rb +27 -0
  54. data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
  55. data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
  56. data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
  57. data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
  58. data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
  59. data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
  60. data/app/models/rails_pulse/operation.rb +13 -4
  61. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
  62. data/app/models/rails_pulse/request.rb +5 -1
  63. data/app/models/rails_pulse/route.rb +71 -21
  64. data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
  65. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
  66. data/app/models/rails_pulse/routes/tables/index.rb +8 -6
  67. data/app/models/rails_pulse/summary.rb +21 -10
  68. data/app/models/rails_pulse/tables/base.rb +4 -2
  69. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
  70. data/app/services/rails_pulse/exception_capture_service.rb +228 -0
  71. data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
  72. data/app/services/rails_pulse/operations/change_point.rb +144 -0
  73. data/app/services/rails_pulse/operations/compare.rb +92 -0
  74. data/app/services/rails_pulse/operations/comparison.rb +126 -0
  75. data/app/services/rails_pulse/operations/metric.rb +63 -0
  76. data/app/services/rails_pulse/operations/series.rb +84 -0
  77. data/app/services/rails_pulse/operations/subject.rb +77 -0
  78. data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
  79. data/app/services/rails_pulse/route_merger.rb +99 -0
  80. data/app/services/rails_pulse/route_migrator.rb +139 -0
  81. data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
  82. data/app/services/rails_pulse/route_recognizer.rb +47 -0
  83. data/app/services/rails_pulse/summary_service.rb +45 -0
  84. data/app/services/rails_pulse/tag_filter_service.rb +5 -3
  85. data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
  86. data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
  87. data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
  88. data/app/views/layouts/rails_pulse/application.html.erb +4 -4
  89. data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
  90. data/app/views/rails_pulse/components/_panel.html.erb +16 -6
  91. data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
  92. data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
  93. data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
  94. data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
  95. data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
  96. data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
  97. data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
  98. data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
  99. data/app/views/rails_pulse/deployments/index.html.erb +15 -0
  100. data/app/views/rails_pulse/deployments/show.html.erb +62 -0
  101. data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
  102. data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
  103. data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
  104. data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
  105. data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
  106. data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
  107. data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
  108. data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
  109. data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
  110. data/app/views/rails_pulse/requests/_table.html.erb +1 -1
  111. data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
  112. data/app/views/rails_pulse/routes/_table.html.erb +22 -8
  113. data/app/views/rails_pulse/routes/index.html.erb +5 -1
  114. data/app/views/rails_pulse/routes/show.html.erb +5 -0
  115. data/app/views/rails_pulse/storage/show.html.erb +123 -0
  116. data/config/routes.rb +13 -4
  117. data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
  118. data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
  119. data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
  120. data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
  121. data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
  122. data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
  123. data/db/rails_pulse_schema.rb +54 -6
  124. data/lib/generators/rails_pulse/base_methods.rb +2 -0
  125. data/lib/generators/rails_pulse/install_generator.rb +5 -0
  126. data/lib/generators/rails_pulse/schema_parser.rb +22 -2
  127. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
  128. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
  129. data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
  130. data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
  131. data/lib/rails_pulse/cleanup_service.rb +110 -15
  132. data/lib/rails_pulse/configuration.rb +125 -6
  133. data/lib/rails_pulse/engine.rb +32 -17
  134. data/lib/rails_pulse/installers/config_updater.rb +202 -0
  135. data/lib/rails_pulse/job_run_collector.rb +43 -16
  136. data/lib/rails_pulse/middleware/asset_server.rb +11 -0
  137. data/lib/rails_pulse/middleware/request_collector.rb +9 -4
  138. data/lib/rails_pulse/packaged_assets.rb +147 -0
  139. data/lib/rails_pulse/route_indexes.rb +66 -0
  140. data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
  141. data/lib/rails_pulse/tracker.rb +29 -10
  142. data/lib/rails_pulse/version.rb +1 -1
  143. data/lib/rails_pulse.rb +1 -0
  144. data/lib/rails_pulse_server.ru +16 -47
  145. data/lib/tasks/rails_pulse.rake +83 -4
  146. data/lib/tasks/rails_pulse_assets.rake +15 -0
  147. data/lib/tasks/rails_pulse_tasks.rake +29 -0
  148. data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
  149. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  150. data/public/rails-pulse-assets/rails-pulse.js +18 -117
  151. metadata +78 -37
  152. data/Rakefile +0 -554
  153. data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
  154. data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
  155. data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
  156. data/app/javascript/rails_pulse/theme.js +0 -416
  157. data/config/brakeman.ignore +0 -190
  158. data/config/brakeman.yml +0 -68
  159. data/config/initializers/rails_pulse.rb +0 -287
  160. data/public/rails-pulse-assets/csp-test.js +0 -120
  161. data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
  162. data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
  163. data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
  164. data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
  165. data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
  166. data/vendor/assets/javascripts/rails-pulse.js +0 -157
  167. data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
  168. data/vendor/assets/stylesheets/rails-pulse.css +0 -1
  169. data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
@@ -56,28 +56,12 @@
56
56
  flex-direction: column;
57
57
  }
58
58
 
59
- .sidebar-menu__group-label {
60
- color: var(--color-text-subtle);
61
- font-size: var(--text-xs);
62
- font-weight: var(--font-medium);
63
- padding: var(--size-1_5) var(--size-2);
64
- }
65
-
66
59
  .sidebar-menu__items {
67
60
  display: flex;
68
61
  flex-direction: column;
69
62
  row-gap: var(--size-1);
70
63
  }
71
64
 
72
- .sidebar-menu__sub {
73
- border-inline-start-width: var(--border);
74
- display: flex;
75
- flex-direction: column;
76
- margin-inline-start: var(--size-4);
77
- padding: var(--size-0_5) var(--size-2);
78
- row-gap: var(--size-1);
79
- }
80
-
81
65
  /* Sheet component styles for mobile menu */
82
66
  .sheet {
83
67
  border: 0;
@@ -0,0 +1,187 @@
1
+ /* Storage status page */
2
+
3
+ .health-badge-link {
4
+ text-decoration: none;
5
+ color: inherit;
6
+ }
7
+
8
+ .health-badge-link:hover {
9
+ text-decoration: none;
10
+ }
11
+
12
+ .health-badge-link:hover .badge {
13
+ background-color: var(--color-secondary);
14
+ }
15
+
16
+ .storage-overview-card {
17
+ min-height: 6.5rem;
18
+ }
19
+
20
+ .storage-panel-stats {
21
+ display: grid;
22
+ grid-template-columns: repeat(4, minmax(0, 1fr));
23
+ gap: var(--size-4);
24
+ margin-bottom: var(--size-4);
25
+ padding-bottom: var(--size-4);
26
+ border-bottom: 1px solid var(--color-border);
27
+ }
28
+
29
+ .storage-panel-tables {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: var(--size-3);
33
+ }
34
+
35
+ .storage-panel-row {
36
+ display: grid;
37
+ grid-template-columns: 7.5rem 1fr auto;
38
+ gap: var(--size-3);
39
+ align-items: center;
40
+ }
41
+
42
+ .storage-panel-row__label {
43
+ white-space: nowrap;
44
+ overflow: hidden;
45
+ text-overflow: ellipsis;
46
+ }
47
+
48
+ .storage-panel-row__count {
49
+ white-space: nowrap;
50
+ font-variant-numeric: tabular-nums;
51
+ }
52
+
53
+ .storage-fill--compact {
54
+ margin-block: 0;
55
+ height: 0.375rem;
56
+ }
57
+
58
+ @media (max-width: 768px) {
59
+ .storage-panel-stats {
60
+ grid-template-columns: repeat(2, minmax(0, 1fr));
61
+ }
62
+
63
+ .storage-panel-row {
64
+ grid-template-columns: 1fr auto;
65
+ }
66
+
67
+ .storage-panel-row .storage-fill {
68
+ grid-column: 1 / -1;
69
+ order: 3;
70
+ }
71
+ }
72
+
73
+ .storage-health--healthy {
74
+ color: var(--color-positive);
75
+ }
76
+
77
+ .storage-health--warning {
78
+ color: var(--color-warning-text);
79
+ }
80
+
81
+ .storage-health--critical,
82
+ .storage-health--disabled,
83
+ .storage-health--error {
84
+ color: var(--color-negative);
85
+ }
86
+
87
+ .storage-tables {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: var(--size-5);
91
+ }
92
+
93
+ .storage-table {
94
+ padding-bottom: var(--size-5);
95
+ border-bottom: 1px solid var(--color-border);
96
+ }
97
+
98
+ .storage-table:last-child {
99
+ padding-bottom: 0;
100
+ border-bottom: none;
101
+ }
102
+
103
+ .storage-table__header {
104
+ display: flex;
105
+ align-items: flex-start;
106
+ justify-content: space-between;
107
+ gap: var(--size-4);
108
+ }
109
+
110
+ .storage-table__counts {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: var(--size-2);
114
+ flex-shrink: 0;
115
+ }
116
+
117
+ .storage-table__percent {
118
+ font-variant-numeric: tabular-nums;
119
+ }
120
+
121
+ .storage-table__percent--healthy {
122
+ color: var(--color-positive);
123
+ border-color: color-mix(in srgb, var(--color-positive) 30%, transparent);
124
+ }
125
+
126
+ .storage-table__percent--warning {
127
+ color: var(--color-warning-text);
128
+ border-color: color-mix(in srgb, #eab308 40%, transparent);
129
+ }
130
+
131
+ .storage-table__percent--critical,
132
+ .storage-table__percent--error {
133
+ color: var(--color-negative);
134
+ border-color: color-mix(in srgb, var(--color-negative) 30%, transparent);
135
+ }
136
+
137
+ .storage-table__percent--uncapped {
138
+ color: var(--color-text-subtle);
139
+ }
140
+
141
+ .storage-fill {
142
+ height: 0.5rem;
143
+ margin-block: var(--size-2);
144
+ border-radius: var(--rounded-full, 999px);
145
+ background: var(--color-secondary);
146
+ overflow: hidden;
147
+ }
148
+
149
+ .storage-fill__bar {
150
+ height: 100%;
151
+ border-radius: inherit;
152
+ min-width: 0;
153
+ }
154
+
155
+ .storage-fill__bar--healthy {
156
+ background: var(--color-positive);
157
+ }
158
+
159
+ .storage-fill__bar--warning {
160
+ background: #eab308;
161
+ }
162
+
163
+ .storage-fill__bar--critical,
164
+ .storage-fill__bar--error {
165
+ background: var(--color-negative);
166
+ }
167
+
168
+ .storage-fill__bar--uncapped {
169
+ background: transparent;
170
+ }
171
+
172
+ .storage-table__meta {
173
+ display: flex;
174
+ flex-wrap: wrap;
175
+ gap: var(--size-1) var(--size-4);
176
+ }
177
+
178
+ .storage-table__meta span + span::before {
179
+ content: none;
180
+ }
181
+
182
+ @media (max-width: 768px) {
183
+ .storage-table__header {
184
+ flex-direction: column;
185
+ gap: var(--size-2);
186
+ }
187
+ }
@@ -31,20 +31,6 @@
31
31
  padding-right: 3px !important;
32
32
  }
33
33
 
34
- /* Individual Tag */
35
- .tag {
36
- display: inline-flex;
37
- align-items: center;
38
- gap: 0.25rem;
39
- padding: 0.25rem 0.5rem;
40
- background-color: var(--color-background-secondary);
41
- border: 1px solid var(--color-border);
42
- border-radius: 9999px;
43
- font-size: 0.875rem;
44
- line-height: 1.25;
45
- white-space: nowrap;
46
- }
47
-
48
34
  /* Tag chips — pill shape with subtle translucent fill */
49
35
  .tag-list .badge {
50
36
  border-radius: 9999px;
@@ -18,7 +18,7 @@
18
18
 
19
19
  .time-range-trigger:hover {
20
20
  background: var(--color-secondary);
21
- border-color: var(--color-border-emphasis);
21
+ border-color: var(--color-border-dark);
22
22
  }
23
23
 
24
24
  .time-range-trigger svg {
@@ -28,7 +28,7 @@
28
28
 
29
29
  .time-range-trigger:has(+ [popover]:popover-open) {
30
30
  background: var(--color-secondary);
31
- border-color: var(--color-border-emphasis);
31
+ border-color: var(--color-border-dark);
32
32
  }
33
33
 
34
34
  .time-range-trigger:has(+ [popover]:popover-open) svg {
@@ -36,16 +36,16 @@
36
36
  }
37
37
 
38
38
  /* Light mode improvements */
39
- html[data-color-scheme="light"] .time-range-trigger {
39
+ html:not([data-color-scheme="dark"]) .time-range-trigger {
40
40
  border-color: rgba(0, 0, 0, 0.2);
41
41
  }
42
42
 
43
- html[data-color-scheme="light"] .time-range-trigger:hover {
43
+ html:not([data-color-scheme="dark"]) .time-range-trigger:hover {
44
44
  background: rgba(0, 0, 0, 0.05);
45
45
  border-color: rgba(0, 0, 0, 0.3);
46
46
  }
47
47
 
48
- html[data-color-scheme="light"] .time-range-trigger:has(+ [popover]:popover-open) {
48
+ html:not([data-color-scheme="dark"]) .time-range-trigger:has(+ [popover]:popover-open) {
49
49
  background: rgba(0, 0, 0, 0.05);
50
50
  border-color: rgba(0, 0, 0, 0.3);
51
51
  }
@@ -3,37 +3,20 @@
3
3
  .w-4 { width: 1rem; }
4
4
  .w-6 { width: 1.5rem; }
5
5
  .w-8 { width: 2rem; }
6
- .w-12 { width: 3rem; }
7
6
  .w-16 { width: 4rem; }
8
7
  .w-20 { width: 5rem; }
9
8
  .w-24 { width: 6rem; }
10
9
  .w-28 { width: 7rem; }
11
10
  .w-32 { width: 8rem; }
12
11
  .w-36 { width: 9rem; }
13
- .w-40 { width: 10rem; }
14
- .w-44 { width: 11rem; }
15
12
  .w-48 { width: 12rem; }
16
- .w-52 { width: 13rem; }
17
13
  .w-56 { width: 14rem; }
18
14
  .w-60 { width: 15rem; }
19
- .w-64 { width: 16rem; }
20
15
 
21
16
  /* Min-width utilities */
22
- .min-w-0 { min-width: 0; }
23
- .min-w-4 { min-width: 1rem; }
24
- .min-w-8 { min-width: 2rem; }
25
- .min-w-12 { min-width: 3rem; }
26
- .min-w-16 { min-width: 4rem; }
27
- .min-w-20 { min-width: 5rem; }
28
- .min-w-24 { min-width: 6rem; }
29
- .min-w-32 { min-width: 8rem; }
30
17
 
31
18
  /* Max-width utilities */
32
- .max-w-xs { max-width: 20rem; }
33
- .max-w-sm { max-width: 24rem; }
34
- .max-w-md { max-width: 28rem; }
35
19
  .max-w-lg { max-width: 32rem; }
36
- .max-w-xl { max-width: 36rem; }
37
20
 
38
21
  /* Table layout utilities */
39
22
  .table-fixed { table-layout: fixed; }
@@ -53,7 +53,7 @@ module ChartTableConcern
53
53
  end
54
54
 
55
55
  def setup_chart_and_table_data
56
- ransack_params = params[:q] || {}
56
+ ransack_params = self.ransack_params
57
57
 
58
58
  unless partial_request?
59
59
  # Setup chart data first using original time range (no sorting from table)
@@ -172,11 +172,18 @@ module ChartTableConcern
172
172
  action_name == "show"
173
173
  end
174
174
 
175
+ # Hook: override to return param keys that should be excluded from chart queries.
176
+ # Use this when a filter requires a JOIN that chart queries don't perform
177
+ # (e.g. route path/action filters that need rails_pulse_routes joined).
178
+ def chart_filter_exclusions
179
+ []
180
+ end
181
+
175
182
  # Builds ransack parameters for chart queries
176
183
  # Common pattern: time range + optional duration filter + resource scope
177
184
  # Handles "recent" mode where @page_timings.start_time/@end_time may be nil
178
185
  def build_chart_ransack_params(ransack_params)
179
- base_params = ransack_params.except(:s)
186
+ base_params = ransack_params.except(:s, *chart_filter_exclusions)
180
187
 
181
188
  # Add time filters if we have time boundaries (not in "recent" mode)
182
189
  if @page_timings&.start_time && @page_timings&.end_time
@@ -196,7 +203,7 @@ module ChartTableConcern
196
203
 
197
204
  # Scope to specific resource on show pages
198
205
  if show_action?
199
- base_params.merge(summarizable_id_eq: current_resource.id)
206
+ base_params.merge(resource_id_scope)
200
207
  else
201
208
  base_params
202
209
  end
@@ -308,4 +315,9 @@ module ChartTableConcern
308
315
  def current_resource
309
316
  nil
310
317
  end
318
+
319
+ # Ransack scope for chart summaries on show pages. Override to customize (e.g. IN queries).
320
+ def resource_id_scope
321
+ { summarizable_id_eq: current_resource.id }
322
+ end
311
323
  end
@@ -0,0 +1,18 @@
1
+ # RansackParamsConcern
2
+ #
3
+ # Every filterable page reads its Ransack conditions from params[:q]. The
4
+ # dashboard's own forms always send a hash, but a hand-edited URL can make it
5
+ # a string or an array (`?q=foo`), and each caller would then invoke Hash
6
+ # methods on it and 500. Normalise once, here.
7
+ module RansackParamsConcern
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ # params[:q] as an ActionController::Parameters hash, or an empty one when
13
+ # the value is absent or not hash-shaped.
14
+ def ransack_params
15
+ q = params[:q]
16
+ q.respond_to?(:permit) ? q : ActionController::Parameters.new({})
17
+ end
18
+ end
@@ -5,9 +5,9 @@
5
5
  # Supports both page-specific duration filters and global performance threshold filters.
6
6
  module ResponseRangeConcern
7
7
  extend ActiveSupport::Concern
8
+ include RansackParamsConcern
8
9
 
9
10
  def setup_duration_range(type = :route)
10
- ransack_params = params[:q] || {}
11
11
  thresholds = RailsPulse.configuration.public_send("#{type}_thresholds")
12
12
 
13
13
  # Check all duration-related parameters
@@ -11,6 +11,7 @@
11
11
  # Normalizes times to beginning/end of hour or day based on range duration.
12
12
  module TimeRangeConcern
13
13
  extend ActiveSupport::Concern
14
+ include RansackParamsConcern
14
15
 
15
16
  included do
16
17
  # Define the constant in the including class - ordered by most common usage
@@ -40,8 +41,6 @@ module TimeRangeConcern
40
41
  end_time = Time.zone.now
41
42
  selected_time_range = default_key
42
43
 
43
- ransack_params = params[:q] || {}
44
-
45
44
  # Priority 1: Page-specific preset from dropdown (check this first!)
46
45
  if ransack_params[:period_start_range].present? && ransack_params[:period_start_range].to_sym != :custom
47
46
  # Predefined time range from dropdown
@@ -59,22 +58,30 @@ module TimeRangeConcern
59
58
  elsif ransack_params[:period_start_range].present? && ransack_params[:period_start_range].to_sym == :custom && ransack_params[:custom_date_range].present? && ransack_params[:custom_date_range].include?(" to ")
60
59
  # Custom datetime range from custom range picker
61
60
  dates = ransack_params[:custom_date_range].split(" to ")
62
- start_time = parse_time_param(dates[0].strip)
63
- end_time = parse_time_param(dates[1].strip)
64
- selected_time_range = :custom
61
+ custom_start = parse_time_param(dates[0].strip)
62
+ custom_end = parse_time_param(dates[1].strip)
63
+ if custom_start && custom_end
64
+ start_time = custom_start
65
+ end_time = custom_end
66
+ selected_time_range = :custom
67
+ end
65
68
  # Priority 3: Page-specific filters (chart zoom)
66
69
  elsif ransack_params[:occurred_at_gteq].present? && ransack_params[:occurred_at_lt].present?
67
70
  # Custom time range from chart zoom
68
- start_time = parse_time_param(ransack_params[:occurred_at_gteq])
69
- end_time = parse_time_param(ransack_params[:occurred_at_lt])
70
- selected_time_range = :custom
71
+ zoom_start = parse_time_param(ransack_params[:occurred_at_gteq])
72
+ zoom_end = parse_time_param(ransack_params[:occurred_at_lt])
73
+ if zoom_start && zoom_end
74
+ start_time = zoom_start
75
+ end_time = zoom_end
76
+ selected_time_range = :custom
77
+ end
71
78
  # Priority 4: Time range selector (from session)
72
79
  elsif session[:time_range_preference].present?
73
80
  preference = session[:time_range_preference]
74
81
  if preference.is_a?(Hash) && preference["type"] == "custom"
75
82
  # Custom range from time range selector
76
- start_time = parse_time_param(preference["start_time"])
77
- end_time = parse_time_param(preference["end_time"])
83
+ start_time = parse_time_param(preference["start_time"]) || start_time
84
+ end_time = parse_time_param(preference["end_time"]) || end_time
78
85
  selected_time_range = :custom
79
86
  else
80
87
  # Preset from time range selector
@@ -90,8 +97,8 @@ module TimeRangeConcern
90
97
  end
91
98
  # Priority 5: Global filters (from session)
92
99
  elsif session_global_filters["start_time"].present? || session_global_filters["end_time"].present?
93
- start_time = parse_time_param(session_global_filters["start_time"]) if session_global_filters["start_time"].present?
94
- end_time = parse_time_param(session_global_filters["end_time"]) if session_global_filters["end_time"].present?
100
+ start_time = parse_time_param(session_global_filters["start_time"]) || start_time
101
+ end_time = parse_time_param(session_global_filters["end_time"]) || end_time
95
102
  selected_time_range = :custom
96
103
  end
97
104
  # Priority 6: Default time range (already set above)
@@ -112,17 +119,24 @@ module TimeRangeConcern
112
119
 
113
120
  private
114
121
 
122
+ # Returns nil for anything that does not parse, so a hand-edited or stale
123
+ # value falls back to the default range instead of raising.
115
124
  def parse_time_param(param)
116
125
  case param
117
126
  when Time, DateTime
118
127
  param.in_time_zone
119
128
  when String
129
+ return nil if param.blank?
130
+
120
131
  # Parse as server local time (not UTC, not Time.zone)
121
132
  # This ensures flatpickr datetime strings are interpreted in server's timezone
122
133
  Time.parse(param).localtime
134
+ when Numeric
135
+ Time.zone.at(param)
123
136
  else
124
- # Assume it's an integer timestamp
125
- Time.zone.at(param.to_i)
137
+ nil
126
138
  end
139
+ rescue ArgumentError, TypeError, RangeError
140
+ nil
127
141
  end
128
142
  end