rails_pulse 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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +638 -0
  4. data/Rakefile +207 -0
  5. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  6. data/app/assets/images/rails_pulse/menu.svg +1 -0
  7. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  8. data/app/assets/images/rails_pulse/request.png +0 -0
  9. data/app/assets/images/rails_pulse/routes.png +0 -0
  10. data/app/assets/stylesheets/rails_pulse/application.css +102 -0
  11. data/app/assets/stylesheets/rails_pulse/components/alert.css +24 -0
  12. data/app/assets/stylesheets/rails_pulse/components/badge.css +58 -0
  13. data/app/assets/stylesheets/rails_pulse/components/base.css +79 -0
  14. data/app/assets/stylesheets/rails_pulse/components/breadcrumb.css +31 -0
  15. data/app/assets/stylesheets/rails_pulse/components/button.css +99 -0
  16. data/app/assets/stylesheets/rails_pulse/components/card.css +19 -0
  17. data/app/assets/stylesheets/rails_pulse/components/chart.css +18 -0
  18. data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +86 -0
  19. data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +9 -0
  20. data/app/assets/stylesheets/rails_pulse/components/dialog.css +56 -0
  21. data/app/assets/stylesheets/rails_pulse/components/flash.css +47 -0
  22. data/app/assets/stylesheets/rails_pulse/components/input.css +80 -0
  23. data/app/assets/stylesheets/rails_pulse/components/layouts.css +63 -0
  24. data/app/assets/stylesheets/rails_pulse/components/menu.css +43 -0
  25. data/app/assets/stylesheets/rails_pulse/components/popover.css +36 -0
  26. data/app/assets/stylesheets/rails_pulse/components/prose.css +144 -0
  27. data/app/assets/stylesheets/rails_pulse/components/row.css +24 -0
  28. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +79 -0
  29. data/app/assets/stylesheets/rails_pulse/components/skeleton.css +5 -0
  30. data/app/assets/stylesheets/rails_pulse/components/table.css +37 -0
  31. data/app/assets/stylesheets/rails_pulse/components/utilities.css +36 -0
  32. data/app/controllers/concerns/chart_table_concern.rb +82 -0
  33. data/app/controllers/concerns/response_range_concern.rb +24 -0
  34. data/app/controllers/concerns/time_range_concern.rb +67 -0
  35. data/app/controllers/concerns/zoom_range_concern.rb +40 -0
  36. data/app/controllers/rails_pulse/application_controller.rb +67 -0
  37. data/app/controllers/rails_pulse/assets_controller.rb +33 -0
  38. data/app/controllers/rails_pulse/caches_controller.rb +115 -0
  39. data/app/controllers/rails_pulse/csp_test_controller.rb +57 -0
  40. data/app/controllers/rails_pulse/dashboard_controller.rb +6 -0
  41. data/app/controllers/rails_pulse/operations_controller.rb +219 -0
  42. data/app/controllers/rails_pulse/queries_controller.rb +121 -0
  43. data/app/controllers/rails_pulse/requests_controller.rb +69 -0
  44. data/app/controllers/rails_pulse/routes_controller.rb +99 -0
  45. data/app/helpers/rails_pulse/application_helper.rb +111 -0
  46. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +62 -0
  47. data/app/helpers/rails_pulse/cached_component_helper.rb +73 -0
  48. data/app/helpers/rails_pulse/chart_formatters.rb +43 -0
  49. data/app/helpers/rails_pulse/chart_helper.rb +140 -0
  50. data/app/helpers/rails_pulse/formatting_helper.rb +29 -0
  51. data/app/helpers/rails_pulse/status_helper.rb +279 -0
  52. data/app/helpers/rails_pulse/table_helper.rb +54 -0
  53. data/app/javascript/rails_pulse/application.js +119 -0
  54. data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +20 -0
  55. data/app/javascript/rails_pulse/controllers/context_menu_controller.js +16 -0
  56. data/app/javascript/rails_pulse/controllers/dialog_controller.js +21 -0
  57. data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +67 -0
  58. data/app/javascript/rails_pulse/controllers/form_controller.js +39 -0
  59. data/app/javascript/rails_pulse/controllers/icon_controller.js +170 -0
  60. data/app/javascript/rails_pulse/controllers/index_controller.js +230 -0
  61. data/app/javascript/rails_pulse/controllers/menu_controller.js +60 -0
  62. data/app/javascript/rails_pulse/controllers/pagination_controller.js +69 -0
  63. data/app/javascript/rails_pulse/controllers/popover_controller.js +91 -0
  64. data/app/javascript/rails_pulse/controllers/timezone_controller.js +106 -0
  65. data/app/javascript/rails_pulse/theme.js +416 -0
  66. data/app/jobs/rails_pulse/application_job.rb +4 -0
  67. data/app/jobs/rails_pulse/cleanup_job.rb +21 -0
  68. data/app/mailers/rails_pulse/application_mailer.rb +6 -0
  69. data/app/models/rails_pulse/application_record.rb +7 -0
  70. data/app/models/rails_pulse/component_cache_key.rb +33 -0
  71. data/app/models/rails_pulse/dashboard/charts/average_response_time.rb +27 -0
  72. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +37 -0
  73. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +59 -0
  74. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +45 -0
  75. data/app/models/rails_pulse/operation.rb +87 -0
  76. data/app/models/rails_pulse/queries/cards/average_query_times.rb +52 -0
  77. data/app/models/rails_pulse/queries/cards/execution_rate.rb +57 -0
  78. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +71 -0
  79. data/app/models/rails_pulse/queries/charts/average_query_times.rb +112 -0
  80. data/app/models/rails_pulse/query.rb +58 -0
  81. data/app/models/rails_pulse/request.rb +64 -0
  82. data/app/models/rails_pulse/requests/charts/average_response_times.rb +99 -0
  83. data/app/models/rails_pulse/requests/charts/operations_chart.rb +35 -0
  84. data/app/models/rails_pulse/route.rb +77 -0
  85. data/app/models/rails_pulse/routes/cards/average_response_times.rb +54 -0
  86. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +73 -0
  87. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +73 -0
  88. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +59 -0
  89. data/app/models/rails_pulse/routes/charts/average_response_times.rb +115 -0
  90. data/app/models/rails_pulse/routes/tables/index.rb +63 -0
  91. data/app/services/rails_pulse/sql_query_normalizer.rb +124 -0
  92. data/app/views/layouts/rails_pulse/_menu_items.html.erb +19 -0
  93. data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +44 -0
  94. data/app/views/layouts/rails_pulse/application.html.erb +72 -0
  95. data/app/views/rails_pulse/caches/show.html.erb +9 -0
  96. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +12 -0
  97. data/app/views/rails_pulse/components/_code_panel.html.erb +12 -0
  98. data/app/views/rails_pulse/components/_metric_card.html.erb +55 -0
  99. data/app/views/rails_pulse/components/_metric_row.html.erb +9 -0
  100. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +241 -0
  101. data/app/views/rails_pulse/components/_panel.html.erb +56 -0
  102. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +15 -0
  103. data/app/views/rails_pulse/components/_table.html.erb +50 -0
  104. data/app/views/rails_pulse/components/_table_head.html.erb +20 -0
  105. data/app/views/rails_pulse/components/_table_pagination.html.erb +45 -0
  106. data/app/views/rails_pulse/components/_time_period.html.erb +16 -0
  107. data/app/views/rails_pulse/csp_test/show.html.erb +207 -0
  108. data/app/views/rails_pulse/dashboard/charts/_bar_chart.html.erb +1 -0
  109. data/app/views/rails_pulse/dashboard/index.html.erb +64 -0
  110. data/app/views/rails_pulse/dashboard/tables/_routes_table.html.erb +32 -0
  111. data/app/views/rails_pulse/dashboard/tables/_standard_table.html.erb +1 -0
  112. data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +43 -0
  113. data/app/views/rails_pulse/operations/_operation_analysis_database.html.erb +12 -0
  114. data/app/views/rails_pulse/operations/_operation_analysis_generic.html.erb +15 -0
  115. data/app/views/rails_pulse/operations/_operation_analysis_other.html.erb +69 -0
  116. data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +39 -0
  117. data/app/views/rails_pulse/operations/show.html.erb +79 -0
  118. data/app/views/rails_pulse/queries/_show_table.html.erb +19 -0
  119. data/app/views/rails_pulse/queries/_table.html.erb +31 -0
  120. data/app/views/rails_pulse/queries/index.html.erb +64 -0
  121. data/app/views/rails_pulse/queries/show.html.erb +86 -0
  122. data/app/views/rails_pulse/requests/_operations.html.erb +85 -0
  123. data/app/views/rails_pulse/requests/_table.html.erb +31 -0
  124. data/app/views/rails_pulse/requests/index.html.erb +64 -0
  125. data/app/views/rails_pulse/requests/show.html.erb +44 -0
  126. data/app/views/rails_pulse/routes/_table.html.erb +29 -0
  127. data/app/views/rails_pulse/routes/index.html.erb +65 -0
  128. data/app/views/rails_pulse/routes/show.html.erb +67 -0
  129. data/app/views/rails_pulse/skeletons/_chart.html.erb +3 -0
  130. data/app/views/rails_pulse/skeletons/_metric_card.html.erb +20 -0
  131. data/app/views/rails_pulse/skeletons/_panel.html.erb +19 -0
  132. data/app/views/rails_pulse/skeletons/_table.html.erb +8 -0
  133. data/config/importmap.rb +12 -0
  134. data/config/initializers/rails_charts_csp_patch.rb +83 -0
  135. data/config/initializers/rails_pulse.rb +198 -0
  136. data/config/routes.rb +16 -0
  137. data/db/migrate/20250227235904_create_routes.rb +12 -0
  138. data/db/migrate/20250227235915_create_requests.rb +19 -0
  139. data/db/migrate/20250228000000_create_queries.rb +14 -0
  140. data/db/migrate/20250228000056_create_operations.rb +24 -0
  141. data/lib/generators/rails_pulse/install_generator.rb +17 -0
  142. data/lib/generators/rails_pulse/templates/rails_pulse.rb +198 -0
  143. data/lib/rails_pulse/cleanup_service.rb +212 -0
  144. data/lib/rails_pulse/configuration.rb +176 -0
  145. data/lib/rails_pulse/engine.rb +88 -0
  146. data/lib/rails_pulse/middleware/asset_server.rb +84 -0
  147. data/lib/rails_pulse/middleware/request_collector.rb +120 -0
  148. data/lib/rails_pulse/migration.rb +29 -0
  149. data/lib/rails_pulse/subscribers/operation_subscriber.rb +280 -0
  150. data/lib/rails_pulse/version.rb +3 -0
  151. data/lib/rails_pulse.rb +38 -0
  152. data/lib/tasks/rails_pulse_tasks.rake +138 -0
  153. data/public/rails-pulse-assets/csp-test.js +110 -0
  154. data/public/rails-pulse-assets/rails-pulse-icons.js +89 -0
  155. data/public/rails-pulse-assets/rails-pulse-icons.js.map +13 -0
  156. data/public/rails-pulse-assets/rails-pulse.css +1 -0
  157. data/public/rails-pulse-assets/rails-pulse.css.map +1 -0
  158. data/public/rails-pulse-assets/rails-pulse.js +183 -0
  159. data/public/rails-pulse-assets/rails-pulse.js.map +7 -0
  160. metadata +339 -0
@@ -0,0 +1,416 @@
1
+
2
+ (function (root, factory) {
3
+ if (typeof define === 'function' && define.amd) {
4
+ // AMD. Register as an anonymous module.
5
+ define(['exports', 'echarts'], factory);
6
+ } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
7
+ // CommonJS
8
+ factory(exports, require('echarts'));
9
+ } else {
10
+ // Browser globals
11
+ factory({}, root.echarts);
12
+ }
13
+ }(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this, function (exports, echarts) {
14
+ var log = function (msg) {
15
+ if (typeof console !== 'undefined') {
16
+ console && console.error && console.error(msg);
17
+ }
18
+ };
19
+ if (!echarts) {
20
+ log('ECharts is not Loaded');
21
+ return;
22
+ }
23
+ echarts.registerTheme('railspulse', {
24
+ "color": [
25
+ "#ffc91f",
26
+ "#ffde66",
27
+ "#fbedbf",
28
+ "#ffc91f",
29
+ "#ffc91f",
30
+ "#ffc91f"
31
+ ],
32
+ "backgroundColor": "rgba(255,255,255,0)",
33
+ "textStyle": {},
34
+ "title": {
35
+ "textStyle": {
36
+ "color": "#666666"
37
+ },
38
+ "subtextStyle": {
39
+ "color": "#999999"
40
+ }
41
+ },
42
+ "line": {
43
+ "itemStyle": {
44
+ "borderWidth": "2"
45
+ },
46
+ "lineStyle": {
47
+ "width": "3"
48
+ },
49
+ "symbolSize": "8",
50
+ "symbol": "emptyCircle",
51
+ "smooth": false
52
+ },
53
+ "radar": {
54
+ "itemStyle": {
55
+ "borderWidth": "2"
56
+ },
57
+ "lineStyle": {
58
+ "width": "3"
59
+ },
60
+ "symbolSize": "8",
61
+ "symbol": "emptyCircle",
62
+ "smooth": false
63
+ },
64
+ "bar": {
65
+ "itemStyle": {
66
+ "barBorderWidth": 0,
67
+ "barBorderColor": "#ccc"
68
+ }
69
+ },
70
+ "pie": {
71
+ "itemStyle": {
72
+ "borderWidth": 0,
73
+ "borderColor": "#ccc"
74
+ }
75
+ },
76
+ "scatter": {
77
+ "itemStyle": {
78
+ "borderWidth": 0,
79
+ "borderColor": "#ccc"
80
+ }
81
+ },
82
+ "boxplot": {
83
+ "itemStyle": {
84
+ "borderWidth": 0,
85
+ "borderColor": "#ccc"
86
+ }
87
+ },
88
+ "parallel": {
89
+ "itemStyle": {
90
+ "borderWidth": 0,
91
+ "borderColor": "#ccc"
92
+ }
93
+ },
94
+ "sankey": {
95
+ "itemStyle": {
96
+ "borderWidth": 0,
97
+ "borderColor": "#ccc"
98
+ }
99
+ },
100
+ "funnel": {
101
+ "itemStyle": {
102
+ "borderWidth": 0,
103
+ "borderColor": "#ccc"
104
+ }
105
+ },
106
+ "gauge": {
107
+ "itemStyle": {
108
+ "borderWidth": 0,
109
+ "borderColor": "#ccc"
110
+ }
111
+ },
112
+ "candlestick": {
113
+ "itemStyle": {
114
+ "color": "#ffc91f",
115
+ "color0": "transparent",
116
+ "borderColor": "#ffc91f",
117
+ "borderColor0": "#ffc91f",
118
+ "borderWidth": "1"
119
+ }
120
+ },
121
+ "graph": {
122
+ "itemStyle": {
123
+ "borderWidth": 0,
124
+ "borderColor": "#ccc"
125
+ },
126
+ "lineStyle": {
127
+ "width": "1",
128
+ "color": "#cccccc"
129
+ },
130
+ "symbolSize": "8",
131
+ "symbol": "emptyCircle",
132
+ "smooth": false,
133
+ "color": [
134
+ "#ffc91f",
135
+ "#ffde66",
136
+ "#fbedbf",
137
+ "#ffc91f",
138
+ "#ffc91f",
139
+ "#ffc91f"
140
+ ],
141
+ "label": {
142
+ "color": "#ffffff"
143
+ }
144
+ },
145
+ "map": {
146
+ "itemStyle": {
147
+ "areaColor": "#eeeeee",
148
+ "borderColor": "#999999",
149
+ "borderWidth": 0.5
150
+ },
151
+ "label": {
152
+ "color": "#28544e"
153
+ },
154
+ "emphasis": {
155
+ "itemStyle": {
156
+ "areaColor": "rgba(34,195,170,0.25)",
157
+ "borderColor": "#22c3aa",
158
+ "borderWidth": 1
159
+ },
160
+ "label": {
161
+ "color": "#349e8e"
162
+ }
163
+ }
164
+ },
165
+ "geo": {
166
+ "itemStyle": {
167
+ "areaColor": "#eeeeee",
168
+ "borderColor": "#999999",
169
+ "borderWidth": 0.5
170
+ },
171
+ "label": {
172
+ "color": "#28544e"
173
+ },
174
+ "emphasis": {
175
+ "itemStyle": {
176
+ "areaColor": "rgba(34,195,170,0.25)",
177
+ "borderColor": "#22c3aa",
178
+ "borderWidth": 1
179
+ },
180
+ "label": {
181
+ "color": "#349e8e"
182
+ }
183
+ }
184
+ },
185
+ "categoryAxis": {
186
+ "axisLine": {
187
+ "show": true,
188
+ "lineStyle": {
189
+ "color": "#cccccc"
190
+ }
191
+ },
192
+ "axisTick": {
193
+ "show": false,
194
+ "lineStyle": {
195
+ "color": "#333"
196
+ }
197
+ },
198
+ "axisLabel": {
199
+ "show": true,
200
+ "color": "#999999"
201
+ },
202
+ "splitLine": {
203
+ "show": true,
204
+ "lineStyle": {
205
+ "color": [
206
+ "#eeeeee"
207
+ ]
208
+ }
209
+ },
210
+ "splitArea": {
211
+ "show": false,
212
+ "areaStyle": {
213
+ "color": [
214
+ "rgba(250,250,250,0.05)",
215
+ "rgba(200,200,200,0.02)"
216
+ ]
217
+ }
218
+ }
219
+ },
220
+ "valueAxis": {
221
+ "axisLine": {
222
+ "show": true,
223
+ "lineStyle": {
224
+ "color": "#cccccc"
225
+ }
226
+ },
227
+ "axisTick": {
228
+ "show": false,
229
+ "lineStyle": {
230
+ "color": "#333"
231
+ }
232
+ },
233
+ "axisLabel": {
234
+ "show": true,
235
+ "color": "#999999"
236
+ },
237
+ "splitLine": {
238
+ "show": true,
239
+ "lineStyle": {
240
+ "color": [
241
+ "#eeeeee"
242
+ ]
243
+ }
244
+ },
245
+ "splitArea": {
246
+ "show": false,
247
+ "areaStyle": {
248
+ "color": [
249
+ "rgba(250,250,250,0.05)",
250
+ "rgba(200,200,200,0.02)"
251
+ ]
252
+ }
253
+ }
254
+ },
255
+ "logAxis": {
256
+ "axisLine": {
257
+ "show": true,
258
+ "lineStyle": {
259
+ "color": "#cccccc"
260
+ }
261
+ },
262
+ "axisTick": {
263
+ "show": false,
264
+ "lineStyle": {
265
+ "color": "#333"
266
+ }
267
+ },
268
+ "axisLabel": {
269
+ "show": true,
270
+ "color": "#999999"
271
+ },
272
+ "splitLine": {
273
+ "show": true,
274
+ "lineStyle": {
275
+ "color": [
276
+ "#eeeeee"
277
+ ]
278
+ }
279
+ },
280
+ "splitArea": {
281
+ "show": false,
282
+ "areaStyle": {
283
+ "color": [
284
+ "rgba(250,250,250,0.05)",
285
+ "rgba(200,200,200,0.02)"
286
+ ]
287
+ }
288
+ }
289
+ },
290
+ "timeAxis": {
291
+ "axisLine": {
292
+ "show": true,
293
+ "lineStyle": {
294
+ "color": "#cccccc"
295
+ }
296
+ },
297
+ "axisTick": {
298
+ "show": false,
299
+ "lineStyle": {
300
+ "color": "#333"
301
+ }
302
+ },
303
+ "axisLabel": {
304
+ "show": true,
305
+ "color": "#999999"
306
+ },
307
+ "splitLine": {
308
+ "show": true,
309
+ "lineStyle": {
310
+ "color": [
311
+ "#eeeeee"
312
+ ]
313
+ }
314
+ },
315
+ "splitArea": {
316
+ "show": false,
317
+ "areaStyle": {
318
+ "color": [
319
+ "rgba(250,250,250,0.05)",
320
+ "rgba(200,200,200,0.02)"
321
+ ]
322
+ }
323
+ }
324
+ },
325
+ "toolbox": {
326
+ "iconStyle": {
327
+ "borderColor": "#999999"
328
+ },
329
+ "emphasis": {
330
+ "iconStyle": {
331
+ "borderColor": "#666666"
332
+ }
333
+ }
334
+ },
335
+ "legend": {
336
+ "textStyle": {
337
+ "color": "#999999"
338
+ }
339
+ },
340
+ "tooltip": {
341
+ "axisPointer": {
342
+ "lineStyle": {
343
+ "color": "#cccccc",
344
+ "width": 1
345
+ },
346
+ "crossStyle": {
347
+ "color": "#cccccc",
348
+ "width": 1
349
+ }
350
+ }
351
+ },
352
+ "timeline": {
353
+ "lineStyle": {
354
+ "color": "#ffc91f",
355
+ "width": 1
356
+ },
357
+ "itemStyle": {
358
+ "color": "#ffc91f",
359
+ "borderWidth": 1
360
+ },
361
+ "controlStyle": {
362
+ "color": "#ffc91f",
363
+ "borderColor": "#ffc91f",
364
+ "borderWidth": 0.5
365
+ },
366
+ "checkpointStyle": {
367
+ "color": "#ffc91f",
368
+ "borderColor": "#ffc91f"
369
+ },
370
+ "label": {
371
+ "color": "#ffc91f"
372
+ },
373
+ "emphasis": {
374
+ "itemStyle": {
375
+ "color": "#ffc91f"
376
+ },
377
+ "controlStyle": {
378
+ "color": "#ffc91f",
379
+ "borderColor": "#ffc91f",
380
+ "borderWidth": 0.5
381
+ },
382
+ "label": {
383
+ "color": "#ffc91f"
384
+ }
385
+ }
386
+ },
387
+ "visualMap": {
388
+ "color": [
389
+ "#ffc91f",
390
+ "#ffc91f",
391
+ "#ffc91f"
392
+ ]
393
+ },
394
+ "dataZoom": {
395
+ "backgroundColor": "rgba(255,255,255,0)",
396
+ "dataBackgroundColor": "rgba(222,222,222,1)",
397
+ "fillerColor": "rgba(114,230,212,0.25)",
398
+ "handleColor": "#cccccc",
399
+ "handleSize": "100%",
400
+ "textStyle": {
401
+ "color": "#999999"
402
+ }
403
+ },
404
+ "markPoint": {
405
+ "label": {
406
+ "color": "#ffffff"
407
+ },
408
+ "emphasis": {
409
+ "label": {
410
+ "color": "#ffffff"
411
+ }
412
+ }
413
+ }
414
+ });
415
+ }));
416
+
@@ -0,0 +1,4 @@
1
+ module RailsPulse
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,21 @@
1
+ module RailsPulse
2
+ class CleanupJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform
6
+ return unless RailsPulse.configuration.archiving_enabled
7
+
8
+ Rails.logger.info "[RailsPulse::CleanupJob] Starting scheduled cleanup"
9
+
10
+ stats = CleanupService.perform
11
+
12
+ Rails.logger.info "[RailsPulse::CleanupJob] Cleanup completed - #{stats[:total_deleted]} records deleted"
13
+
14
+ stats
15
+ rescue => e
16
+ Rails.logger.error "[RailsPulse::CleanupJob] Cleanup failed: #{e.message}"
17
+ Rails.logger.error e.backtrace.join("\n")
18
+ raise
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ module RailsPulse
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module RailsPulse
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ connects_to(**RailsPulse.connects_to) if RailsPulse.connects_to
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ module RailsPulse
2
+ # Utility class for generating cache keys for cached components
3
+ class ComponentCacheKey
4
+ # Generate a cache key for a specific component type and context
5
+ #
6
+ # The cache key includes several parts:
7
+ # - A namespace prefix to avoid collisions with other cached data
8
+ # - The context (like "routes" or "route_123") to separate different views
9
+ # - The component type (like "average_response_times") to separate different components
10
+ # (Note: Time-based cache expiration is now handled via expires_in option)
11
+ def self.build(id, context = nil)
12
+ [ "rails_pulse_component", id, context ].compact
13
+ end
14
+
15
+ # Generate a cache expiration duration with jitter
16
+ #
17
+ # This returns a duration that can be used with the expires_in option.
18
+ # We add some randomness (jitter) so all components don't expire at exactly
19
+ # the same time, which would cause a "thundering herd" problem where all
20
+ # components recalculate simultaneously and overwhelm the database.
21
+ def self.cache_expires_in
22
+ # Get the configured cache duration (e.g., 5 minutes)
23
+ cache_duration = RailsPulse.configuration.component_cache_duration.to_i
24
+
25
+ # Add up to 25% random jitter to spread out cache expirations
26
+ max_jitter = (cache_duration * 0.25).to_i
27
+ jitter = rand(max_jitter)
28
+
29
+ # Return the base duration plus jitter
30
+ cache_duration + jitter
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ module RailsPulse
2
+ module Dashboard
3
+ module Charts
4
+ class AverageResponseTime
5
+ def to_chart_data
6
+ # Create a range of all dates in the past 2 weeks
7
+ start_date = 2.weeks.ago.beginning_of_day.to_date
8
+ end_date = Time.current.to_date
9
+ date_range = (start_date..end_date)
10
+
11
+ # Get the actual data
12
+ requests = RailsPulse::Request.where("occurred_at >= ?", start_date.beginning_of_day)
13
+ actual_data = requests
14
+ .group_by_day(:occurred_at)
15
+ .average(:duration)
16
+
17
+ # Fill in all dates with zero values for missing days
18
+ date_range.each_with_object({}) do |date, result|
19
+ formatted_date = date.strftime("%b %-d")
20
+ avg_duration = actual_data[date]
21
+ result[formatted_date] = avg_duration&.round(0) || 0
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ module RailsPulse
2
+ module Dashboard
3
+ module Charts
4
+ class P95ResponseTime
5
+ def to_chart_data
6
+ start_date = 2.weeks.ago.beginning_of_day
7
+
8
+ # Performance optimization: Single query instead of N+1 queries (15 queries -> 1 query)
9
+ # Fetch all requests for 2-week period, pre-sorted by date and duration
10
+ # For optimal performance, ensure index exists: (occurred_at, duration)
11
+ requests_by_day = RailsPulse::Request
12
+ .where(occurred_at: start_date..)
13
+ .select("occurred_at, duration, DATE(occurred_at) as request_date")
14
+ .order("request_date, duration")
15
+ .group_by { |r| r.request_date.to_date }
16
+
17
+ # Generate all dates in range and calculate P95 for each
18
+ (start_date.to_date..Time.current.to_date).each_with_object({}) do |date, hash|
19
+ day_requests = requests_by_day[date] || []
20
+
21
+ if day_requests.empty?
22
+ p95_value = 0
23
+ else
24
+ # Calculate P95 from in-memory sorted array (already sorted by DB)
25
+ count = day_requests.length
26
+ p95_index = (count * 0.95).ceil - 1
27
+ p95_value = day_requests[p95_index].duration.round(0)
28
+ end
29
+
30
+ formatted_date = date.strftime("%b %-d")
31
+ hash[formatted_date] = p95_value
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ module RailsPulse
2
+ module Dashboard
3
+ module Tables
4
+ class SlowQueries
5
+ include RailsPulse::FormattingHelper
6
+ def to_table_data
7
+ # Get data for this week
8
+ this_week_start = 1.week.ago.beginning_of_week
9
+ this_week_end = Time.current.end_of_week
10
+
11
+ # Fetch query data for this week
12
+ query_data = RailsPulse::Operation.joins(:query)
13
+ .where(occurred_at: this_week_start..this_week_end)
14
+ .group("rails_pulse_queries.id, rails_pulse_queries.normalized_sql")
15
+ .select("rails_pulse_queries.id, rails_pulse_queries.normalized_sql, AVG(rails_pulse_operations.duration) as avg_duration, COUNT(*) as request_count, MAX(rails_pulse_operations.occurred_at) as last_seen")
16
+ .order("avg_duration DESC")
17
+ .limit(5)
18
+
19
+ # Build data rows
20
+ data_rows = query_data.map do |record|
21
+ {
22
+ query_text: truncate_query(record.normalized_sql),
23
+ query_id: record.id,
24
+ query_link: "/rails_pulse/queries/#{record.id}",
25
+ average_time: record.avg_duration.to_f.round(0),
26
+ request_count: record.request_count,
27
+ last_request: time_ago_in_words(record.last_seen)
28
+ }
29
+ end
30
+
31
+ # Return new structure with columns and data
32
+ {
33
+ columns: [
34
+ { field: :query_text, label: "Query", link_to: :query_link, class: "w-auto" },
35
+ { field: :average_time, label: "Average Time", class: "w-32" },
36
+ { field: :request_count, label: "Requests", class: "w-24" },
37
+ { field: :last_request, label: "Last Request", class: "w-32" }
38
+ ],
39
+ data: data_rows
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def truncate_query(sql)
46
+ return "" if sql.blank?
47
+
48
+ # Remove extra whitespace and truncate
49
+ cleaned_sql = sql.gsub(/\s+/, " ").strip
50
+ if cleaned_sql.length > 38
51
+ "#{cleaned_sql[0..35]}..."
52
+ else
53
+ cleaned_sql
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ module RailsPulse
2
+ module Dashboard
3
+ module Tables
4
+ class SlowRoutes
5
+ include RailsPulse::FormattingHelper
6
+ def to_table_data
7
+ # Get data for this week
8
+ this_week_start = 1.week.ago.beginning_of_week
9
+ this_week_end = Time.current.end_of_week
10
+
11
+ # Fetch route data for this week
12
+ route_data = RailsPulse::Request.joins(:route)
13
+ .where(occurred_at: this_week_start..this_week_end)
14
+ .group("rails_pulse_routes.path, rails_pulse_routes.id")
15
+ .select("rails_pulse_routes.path, rails_pulse_routes.id, AVG(rails_pulse_requests.duration) as avg_duration, COUNT(*) as request_count, MAX(rails_pulse_requests.occurred_at) as last_seen")
16
+ .order("avg_duration DESC")
17
+ .limit(5)
18
+
19
+ # Build data rows
20
+ data_rows = route_data.map do |record|
21
+ {
22
+ route_path: record.path,
23
+ route_id: record.id,
24
+ route_link: "/rails_pulse/routes/#{record.id}",
25
+ average_time: record.avg_duration.to_f.round(0),
26
+ request_count: record.request_count,
27
+ last_request: time_ago_in_words(record.last_seen)
28
+ }
29
+ end
30
+
31
+ # Return new structure with columns and data
32
+ {
33
+ columns: [
34
+ { field: :route_path, label: "Route", link_to: :route_link, class: "w-auto" },
35
+ { field: :average_time, label: "Average Time", class: "w-32" },
36
+ { field: :request_count, label: "Requests", class: "w-24" },
37
+ { field: :last_request, label: "Last Request", class: "w-32" }
38
+ ],
39
+ data: data_rows
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end