devformance 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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +205 -0
  3. data/app/assets/builds/tailwind.css +2 -0
  4. data/app/assets/images/icon.png +0 -0
  5. data/app/assets/images/icon.svg +68 -0
  6. data/app/assets/stylesheets/devmetrics/dashboard.css +476 -0
  7. data/app/assets/stylesheets/devmetrics_live/application.css +10 -0
  8. data/app/assets/tailwind/application.css +1 -0
  9. data/app/channels/application_cable/channel.rb +4 -0
  10. data/app/channels/application_cable/connection.rb +4 -0
  11. data/app/channels/devformance/metrics_channel.rb +25 -0
  12. data/app/controllers/application_controller.rb +4 -0
  13. data/app/controllers/devformance/application_controller.rb +19 -0
  14. data/app/controllers/devformance/icons_controller.rb +21 -0
  15. data/app/controllers/devformance/metrics_controller.rb +41 -0
  16. data/app/controllers/devformance/playground_controller.rb +89 -0
  17. data/app/helpers/application_helper.rb +9 -0
  18. data/app/helpers/metrics_helper.rb +2 -0
  19. data/app/helpers/playground_helper.rb +2 -0
  20. data/app/javascript/devformance/channels/consumer.js +2 -0
  21. data/app/javascript/devformance/channels/index.js +1 -0
  22. data/app/javascript/devformance/controllers/application.js +9 -0
  23. data/app/javascript/devformance/controllers/hello_controller.js +7 -0
  24. data/app/javascript/devformance/controllers/index.js +14 -0
  25. data/app/javascript/devformance/controllers/metrics_controller.js +364 -0
  26. data/app/javascript/devformance/controllers/playground_controller.js +33 -0
  27. data/app/javascript/devmetrics.js +4 -0
  28. data/app/jobs/application_job.rb +7 -0
  29. data/app/jobs/devformance/file_runner_job.rb +318 -0
  30. data/app/mailers/application_mailer.rb +4 -0
  31. data/app/models/application_record.rb +3 -0
  32. data/app/models/devformance/file_result.rb +14 -0
  33. data/app/models/devformance/run.rb +19 -0
  34. data/app/models/devformance/slow_query.rb +5 -0
  35. data/app/views/devformance/metrics/index.html.erb +79 -0
  36. data/app/views/devformance/playground/run.html.erb +63 -0
  37. data/app/views/layouts/devformance/application.html.erb +856 -0
  38. data/app/views/layouts/mailer.html.erb +13 -0
  39. data/app/views/layouts/mailer.text.erb +1 -0
  40. data/app/views/metrics/index.html.erb +334 -0
  41. data/app/views/pwa/manifest.json.erb +22 -0
  42. data/app/views/pwa/service-worker.js +26 -0
  43. data/config/BUSINESS_LOGIC_PLAN.md +1244 -0
  44. data/config/application.rb +31 -0
  45. data/config/boot.rb +4 -0
  46. data/config/cable.yml +17 -0
  47. data/config/cache.yml +16 -0
  48. data/config/credentials.yml.enc +1 -0
  49. data/config/database.yml +98 -0
  50. data/config/deploy.yml +116 -0
  51. data/config/engine_routes.rb +13 -0
  52. data/config/environment.rb +5 -0
  53. data/config/environments/development.rb +84 -0
  54. data/config/environments/production.rb +90 -0
  55. data/config/environments/test.rb +59 -0
  56. data/config/importmap.rb +11 -0
  57. data/config/initializers/assets.rb +7 -0
  58. data/config/initializers/content_security_policy.rb +25 -0
  59. data/config/initializers/filter_parameter_logging.rb +8 -0
  60. data/config/initializers/inflections.rb +16 -0
  61. data/config/locales/en.yml +31 -0
  62. data/config/master.key +1 -0
  63. data/config/puma.rb +41 -0
  64. data/config/queue.yml +22 -0
  65. data/config/recurring.yml +15 -0
  66. data/config/routes.rb +20 -0
  67. data/config/storage.yml +34 -0
  68. data/db/migrate/20260317144616_create_slow_queries.rb +13 -0
  69. data/db/migrate/20260317175630_create_performance_runs.rb +14 -0
  70. data/db/migrate/20260317195043_add_run_id_to_slow_queries.rb +10 -0
  71. data/db/migrate/20260319000001_create_devformance_runs.rb +20 -0
  72. data/db/migrate/20260319000002_create_devformance_file_results.rb +29 -0
  73. data/db/migrate/20260319000003_add_columns_to_slow_queries.rb +7 -0
  74. data/lib/devformance/bullet_log_parser.rb +47 -0
  75. data/lib/devformance/compatibility.rb +12 -0
  76. data/lib/devformance/coverage_setup.rb +33 -0
  77. data/lib/devformance/engine.rb +80 -0
  78. data/lib/devformance/log_writer.rb +29 -0
  79. data/lib/devformance/run_orchestrator.rb +58 -0
  80. data/lib/devformance/sql_instrumentor.rb +29 -0
  81. data/lib/devformance/test_framework/base.rb +43 -0
  82. data/lib/devformance/test_framework/coverage_helper.rb +76 -0
  83. data/lib/devformance/test_framework/detector.rb +26 -0
  84. data/lib/devformance/test_framework/minitest.rb +71 -0
  85. data/lib/devformance/test_framework/registry.rb +24 -0
  86. data/lib/devformance/test_framework/rspec.rb +60 -0
  87. data/lib/devformance/test_helper.rb +42 -0
  88. data/lib/devformance/version.rb +3 -0
  89. data/lib/devformance.rb +196 -0
  90. data/lib/generators/devformance/install/install_generator.rb +73 -0
  91. data/lib/generators/devformance/install/templates/add_columns_to_slow_queries.rb.erb +7 -0
  92. data/lib/generators/devformance/install/templates/add_run_id_to_slow_queries.rb.erb +10 -0
  93. data/lib/generators/devformance/install/templates/create_devformance_file_results.rb.erb +29 -0
  94. data/lib/generators/devformance/install/templates/create_devformance_runs.rb.erb +20 -0
  95. data/lib/generators/devformance/install/templates/create_performance_runs.rb.erb +14 -0
  96. data/lib/generators/devformance/install/templates/create_slow_queries.rb.erb +13 -0
  97. data/lib/generators/devformance/install/templates/initializer.rb +23 -0
  98. data/lib/tasks/devformance.rake +45 -0
  99. data/spec/fixtures/devformance/devformance_run.rb +27 -0
  100. data/spec/fixtures/devformance/file_result.rb +34 -0
  101. data/spec/fixtures/devformance/slow_query.rb +11 -0
  102. data/spec/lib/devmetrics/log_writer_spec.rb +81 -0
  103. data/spec/lib/devmetrics/run_orchestrator_spec.rb +102 -0
  104. data/spec/lib/devmetrics/sql_instrumentor_spec.rb +115 -0
  105. data/spec/models/devmetrics/file_result_spec.rb +87 -0
  106. data/spec/models/devmetrics/run_spec.rb +66 -0
  107. data/spec/models/query_log_spec.rb +21 -0
  108. data/spec/rails_helper.rb +20 -0
  109. data/spec/requests/devmetrics/metrics_controller_spec.rb +149 -0
  110. data/spec/requests/devmetrics_pages_spec.rb +12 -0
  111. data/spec/requests/performance_spec.rb +17 -0
  112. data/spec/requests/slow_perf_spec.rb +9 -0
  113. data/spec/spec_helper.rb +114 -0
  114. data/spec/support/devmetrics_formatter.rb +106 -0
  115. data/spec/support/devmetrics_metrics.rb +37 -0
  116. data/spec/support/factory_bot.rb +3 -0
  117. metadata +200 -0
@@ -0,0 +1,476 @@
1
+ /* ============================================================
2
+ DevMetrics — File Panel UI
3
+ ============================================================ */
4
+
5
+ /* ── File row wrapper ────────────────────────────────────── */
6
+ .dm-file-row {
7
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
8
+ }
9
+
10
+ .dm-file-row:last-child {
11
+ border-bottom: none;
12
+ }
13
+
14
+ /* ── Collapsed header ────────────────────────────────────── */
15
+ .dm-file-header {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 8px;
19
+ padding: 10px 16px;
20
+ cursor: pointer;
21
+ user-select: none;
22
+ transition: background 0.1s;
23
+ min-height: 42px;
24
+ }
25
+
26
+ .dm-file-header:hover {
27
+ background: rgba(255, 255, 255, 0.025);
28
+ }
29
+
30
+ /* ── Chevron ─────────────────────────────────────────────── */
31
+ .dm-chevron {
32
+ font-size: 9px;
33
+ color: #4b5563;
34
+ width: 12px;
35
+ flex-shrink: 0;
36
+ transition: transform 0.15s ease;
37
+ line-height: 1;
38
+ }
39
+
40
+ .dm-chevron--open {
41
+ transform: rotate(90deg);
42
+ }
43
+
44
+ /* ── Status dot ──────────────────────────────────────────── */
45
+ .dm-dot {
46
+ width: 8px;
47
+ height: 8px;
48
+ border-radius: 50%;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ .dm-dot--pending {
53
+ background: #2d3748;
54
+ border: 1px solid #4a5568;
55
+ }
56
+
57
+ .dm-dot--running {
58
+ background: #f59e0b;
59
+ animation: dm-pulse 1.2s ease infinite;
60
+ }
61
+
62
+ .dm-dot--passed {
63
+ background: #10b981;
64
+ }
65
+
66
+ .dm-dot--failed {
67
+ background: #ef4444;
68
+ }
69
+
70
+ .dm-dot--error {
71
+ background: #ef4444;
72
+ }
73
+
74
+ @keyframes dm-pulse {
75
+
76
+ 0%,
77
+ 100% {
78
+ opacity: 1;
79
+ }
80
+
81
+ 50% {
82
+ opacity: 0.3;
83
+ }
84
+ }
85
+
86
+ /* ── File name ───────────────────────────────────────────── */
87
+ .dm-file-name {
88
+ flex: 1;
89
+ font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
90
+ font-size: 12.5px;
91
+ color: #cbd5e1;
92
+ white-space: nowrap;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ }
96
+
97
+ /* ── Header right meta (duration + badges) ───────────────── */
98
+ .dm-file-meta {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 4px;
102
+ flex-shrink: 0;
103
+ }
104
+
105
+ /* ── File loader / spinner ───────────────────────────────── */
106
+ .dm-file-loader {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ margin-left: 4px;
110
+ flex-shrink: 0;
111
+ }
112
+
113
+ .dm-spinner--xs {
114
+ width: 12px;
115
+ height: 12px;
116
+ border: 1.5px solid rgba(255, 255, 255, 0.1);
117
+ border-top-color: #f59e0b;
118
+ border-radius: 50%;
119
+ animation: dm-spin 0.7s linear infinite;
120
+ }
121
+
122
+ @keyframes dm-spin {
123
+ to {
124
+ transform: rotate(360deg);
125
+ }
126
+ }
127
+
128
+ /* ── Progress bar ────────────────────────────────────────── */
129
+ .dm-progress-bar {
130
+ height: 2px;
131
+ background: rgba(255, 255, 255, 0.04);
132
+ }
133
+
134
+ .dm-progress-fill {
135
+ height: 100%;
136
+ background: #f59e0b;
137
+ transition: width 0.3s ease-out;
138
+ }
139
+
140
+ .dm-progress-fill--passed {
141
+ background: #10b981;
142
+ }
143
+
144
+ .dm-progress-fill--failed {
145
+ background: #ef4444;
146
+ }
147
+
148
+ /* ── Expanded panel ──────────────────────────────────────── */
149
+ .dm-panel {
150
+ display: none;
151
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
152
+ height: 260px;
153
+ overflow: hidden;
154
+ }
155
+
156
+ .dm-panel--open {
157
+ display: flex;
158
+ }
159
+
160
+ /* ── Terminal (left pane) ────────────────────────────────── */
161
+ .dm-terminal {
162
+ flex: 1;
163
+ min-width: 0;
164
+ padding: 10px 14px;
165
+ overflow-y: auto;
166
+ background: #080d14;
167
+ font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
168
+ font-size: 11.5px;
169
+ line-height: 1.75;
170
+ }
171
+
172
+ .dm-terminal::-webkit-scrollbar {
173
+ width: 4px;
174
+ }
175
+
176
+ .dm-terminal::-webkit-scrollbar-track {
177
+ background: transparent;
178
+ }
179
+
180
+ .dm-terminal::-webkit-scrollbar-thumb {
181
+ background: #2d3748;
182
+ border-radius: 2px;
183
+ }
184
+
185
+ /* Terminal line types */
186
+ .dm-term-line {
187
+ white-space: pre-wrap;
188
+ word-break: break-all;
189
+ }
190
+
191
+ .dm-term-line--command {
192
+ color: #4b5563;
193
+ font-size: 10.5px;
194
+ margin-bottom: 5px;
195
+ }
196
+
197
+ .dm-term-line--pass {
198
+ color: #4ade80;
199
+ }
200
+
201
+ .dm-term-line--fail {
202
+ color: #f87171;
203
+ }
204
+
205
+ .dm-term-line--error {
206
+ color: #f87171;
207
+ }
208
+
209
+ .dm-term-line--pending {
210
+ color: #fbbf24;
211
+ }
212
+
213
+ .dm-term-line--slow {
214
+ color: #fbbf24;
215
+ }
216
+
217
+ .dm-term-line--n1 {
218
+ color: #fb7185;
219
+ }
220
+
221
+ .dm-term-line--summary {
222
+ color: #e2e8f0;
223
+ font-weight: 600;
224
+ margin-top: 4px;
225
+ }
226
+
227
+ .dm-term-line--info {
228
+ color: #374151;
229
+ }
230
+
231
+ .dm-term-separator {
232
+ border-top: 1px solid rgba(255, 255, 255, 0.07);
233
+ margin: 8px 0 4px;
234
+ }
235
+
236
+ /* ── Sidebar (right pane) ────────────────────────────────── */
237
+ .dm-sidebar {
238
+ width: 188px;
239
+ flex-shrink: 0;
240
+ border-left: 1px solid rgba(255, 255, 255, 0.06);
241
+ background: #060b12;
242
+ padding: 10px 10px 8px;
243
+ display: flex;
244
+ flex-direction: column;
245
+ gap: 6px;
246
+ overflow-y: auto;
247
+ font-size: 11px;
248
+ }
249
+
250
+ .dm-sidebar::-webkit-scrollbar {
251
+ width: 3px;
252
+ }
253
+
254
+ .dm-sidebar::-webkit-scrollbar-thumb {
255
+ background: #1e293b;
256
+ border-radius: 2px;
257
+ }
258
+
259
+ /* ── Stat card ───────────────────────────────────────────── */
260
+ .dm-sidebar-stat {
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 3px;
264
+ background: rgba(255, 255, 255, 0.025);
265
+ border: 1px solid rgba(255, 255, 255, 0.05);
266
+ border-radius: 6px;
267
+ padding: 7px 8px 6px;
268
+ }
269
+
270
+ .dm-sidebar-stat-hd {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 6px;
274
+ }
275
+
276
+ .dm-sidebar-icon {
277
+ width: 14px;
278
+ height: 14px;
279
+ flex-shrink: 0;
280
+ opacity: 0.55;
281
+ }
282
+
283
+ .dm-sidebar-icon--slow {
284
+ color: #f59e0b;
285
+ }
286
+
287
+ .dm-sidebar-icon--n1 {
288
+ color: #fb7185;
289
+ }
290
+
291
+ .dm-sidebar-icon--cov {
292
+ color: #34d399;
293
+ }
294
+
295
+ .dm-sidebar-label {
296
+ flex: 1;
297
+ font-size: 9px;
298
+ font-weight: 700;
299
+ letter-spacing: 0.1em;
300
+ text-transform: uppercase;
301
+ color: #4b5563;
302
+ white-space: nowrap;
303
+ }
304
+
305
+ .dm-sidebar-value {
306
+ font-family: "JetBrains Mono", monospace;
307
+ font-size: 13px;
308
+ font-weight: 700;
309
+ color: #475569;
310
+ line-height: 1;
311
+ min-width: 20px;
312
+ text-align: right;
313
+ }
314
+
315
+ .dm-sidebar-value--slow {
316
+ color: #d97706;
317
+ }
318
+
319
+ .dm-sidebar-value--n1 {
320
+ color: #fb7185;
321
+ }
322
+
323
+ .dm-sidebar-value--cov {
324
+ color: #34d399;
325
+ font-size: 14px;
326
+ }
327
+
328
+ /* ── Item list (below stat header) ──────────────────────── */
329
+ .dm-sidebar-items {
330
+ display: flex;
331
+ flex-direction: column;
332
+ gap: 2px;
333
+ max-height: 52px;
334
+ overflow-y: auto;
335
+ }
336
+
337
+ .dm-sidebar-items:empty {
338
+ display: none;
339
+ }
340
+
341
+ .dm-sidebar-items::-webkit-scrollbar {
342
+ width: 2px;
343
+ }
344
+
345
+ .dm-sidebar-items::-webkit-scrollbar-thumb {
346
+ background: #1e293b;
347
+ border-radius: 1px;
348
+ }
349
+
350
+ .dm-sidebar-item {
351
+ font-family: "JetBrains Mono", monospace;
352
+ font-size: 9.5px;
353
+ color: #374151;
354
+ line-height: 1.4;
355
+ word-break: break-all;
356
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
357
+ padding-top: 2px;
358
+ }
359
+
360
+ .dm-sidebar-item--slow {
361
+ color: #92400e;
362
+ }
363
+
364
+ .dm-sidebar-item--n1 {
365
+ color: #9f1239;
366
+ }
367
+
368
+ /* ── Download log link ───────────────────────────────────── */
369
+ .dm-sidebar-log {
370
+ margin-top: auto;
371
+ padding-top: 4px;
372
+ }
373
+
374
+ .dm-log-link {
375
+ display: none;
376
+ align-items: center;
377
+ gap: 5px;
378
+ color: #3b82f6;
379
+ font-size: 10px;
380
+ text-decoration: none;
381
+ opacity: 0.65;
382
+ transition: opacity 0.15s;
383
+ width: 100%;
384
+ }
385
+
386
+ .dm-log-link:hover {
387
+ opacity: 1;
388
+ }
389
+
390
+ /* ── Header badges ───────────────────────────────────────── */
391
+ .dm-badge {
392
+ display: inline-flex;
393
+ align-items: center;
394
+ font-family: "JetBrains Mono", monospace;
395
+ font-size: 10px;
396
+ font-weight: 500;
397
+ padding: 2px 6px;
398
+ border-radius: 4px;
399
+ white-space: nowrap;
400
+ line-height: 16px;
401
+ letter-spacing: 0.01em;
402
+ }
403
+
404
+ .dm-badge--time {
405
+ background: rgba(255, 255, 255, 0.05);
406
+ color: #6b7280;
407
+ }
408
+
409
+ .dm-badge--n1 {
410
+ background: rgba(239, 68, 68, 0.12);
411
+ color: #f87171;
412
+ border: 1px solid rgba(239, 68, 68, 0.25);
413
+ }
414
+
415
+ .dm-badge--slow {
416
+ background: rgba(245, 158, 11, 0.12);
417
+ color: #fbbf24;
418
+ border: 1px solid rgba(245, 158, 11, 0.25);
419
+ }
420
+
421
+ .dm-badge--cov {
422
+ background: rgba(52, 211, 153, 0.12);
423
+ color: #34d399;
424
+ border: 1px solid rgba(52, 211, 153, 0.25);
425
+ }
426
+
427
+ /* ── Mode Indicator ────────────────────────────────────────── */
428
+ .dm-mode-indicator {
429
+ position: fixed;
430
+ bottom: 16px;
431
+ right: 16px;
432
+ background: #1a1a2e;
433
+ border: 1px solid #fbbf24;
434
+ color: #fbbf24;
435
+ padding: 8px 16px;
436
+ border-radius: 8px;
437
+ font-size: 12px;
438
+ font-weight: 600;
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 10px;
442
+ z-index: 9999;
443
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
444
+ }
445
+
446
+ .dm-mode-indicator--realtime {
447
+ background: #0a1929;
448
+ border-color: #10b981;
449
+ color: #10b981;
450
+ }
451
+
452
+ .dm-mode-dot {
453
+ width: 8px;
454
+ height: 8px;
455
+ border-radius: 50%;
456
+ background: currentColor;
457
+ }
458
+
459
+ .dm-mode-indicator--realtime .dm-mode-dot {
460
+ animation: dm-pulse 1.5s ease infinite;
461
+ }
462
+
463
+ .dm-retry-btn {
464
+ background: transparent;
465
+ border: 1px solid currentColor;
466
+ color: inherit;
467
+ padding: 4px 10px;
468
+ border-radius: 4px;
469
+ font-size: 11px;
470
+ cursor: pointer;
471
+ transition: background 0.2s;
472
+ }
473
+
474
+ .dm-retry-btn:hover {
475
+ background: rgba(255, 255, 255, 0.1);
476
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css.
3
+ *
4
+ * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
5
+ * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
6
+ * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
7
+ * depending on specificity.
8
+ *
9
+ * Consider organizing styles into separate files for maintainability.
10
+ */
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,25 @@
1
+ module Devformance
2
+ class MetricsChannel < ApplicationCable::Channel
3
+ def subscribed
4
+ case params[:stream_type]
5
+ when "run"
6
+ run_id = params[:run_id]
7
+ stream_from "devformance:run:#{run_id}" if run_id.present?
8
+ when "file"
9
+ file_key = params[:file_key]
10
+ run_id = params[:run_id]
11
+ if file_key.present? && run_id.present?
12
+ stream_from "devformance:file:#{file_key}:#{run_id}"
13
+ end
14
+ else
15
+ stream_from "devformance:metrics"
16
+ end
17
+ end
18
+
19
+ def unsubscribed
20
+ stop_all_streams
21
+ end
22
+ end
23
+
24
+ DevformanceChannel = MetricsChannel
25
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ allow_browser versions: :modern
4
+ end
@@ -0,0 +1,19 @@
1
+ module Devformance
2
+ class ApplicationController < ActionController::Base
3
+ helper ::Importmap::ImportmapTagsHelper if defined?(::Importmap::ImportmapTagsHelper)
4
+ helper ::Turbo::FramesHelper if defined?(::Turbo::FramesHelper)
5
+
6
+ allow_browser versions: :modern if respond_to?(:allow_browser)
7
+ protect_from_forgery with: :exception
8
+
9
+ def icon_svg_path
10
+ "/devformance/icon.svg"
11
+ end
12
+ helper_method :icon_svg_path
13
+
14
+ def icon_png_path
15
+ "/devformance/icon.png"
16
+ end
17
+ helper_method :icon_png_path
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Devformance
2
+ class IconsController < ApplicationController
3
+ skip_before_action :verify_authenticity_token
4
+
5
+ def svg
6
+ send_file(
7
+ Devformance::Engine.root.join("app/assets/images/icon.svg"),
8
+ type: "image/svg+xml",
9
+ disposition: "inline"
10
+ )
11
+ end
12
+
13
+ def png
14
+ send_file(
15
+ Devformance::Engine.root.join("app/assets/images/icon.png"),
16
+ type: "image/png",
17
+ disposition: "inline"
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module Devformance
2
+ class MetricsController < ApplicationController
3
+ skip_before_action :verify_authenticity_token, only: [ :run_tests, :run_status ]
4
+
5
+ def index
6
+ end
7
+
8
+ def run_tests
9
+ result = ::Devformance::RunOrchestrator.call
10
+ if result[:error]
11
+ render json: { error: result[:error] }, status: :unprocessable_entity
12
+ else
13
+ render json: result, status: :accepted
14
+ end
15
+ end
16
+
17
+ def run_status
18
+ run = ::Devformance::Run.find_by(run_id: params[:run_id])
19
+ return render json: { error: "Not found" }, status: :not_found unless run
20
+
21
+ render json: {
22
+ run_id: run.run_id,
23
+ status: run.status,
24
+ files: run.file_results.map { |r|
25
+ { file_key: r.file_key, file_path: r.file_path, status: r.status,
26
+ coverage: r.coverage, slow_query_count: r.slow_query_count, n1_count: r.n1_count }
27
+ }
28
+ }
29
+ end
30
+
31
+ def download_log
32
+ result = ::Devformance::FileResult.find_by(
33
+ run_id: params[:run_id], file_key: params[:file_key]
34
+ )
35
+ return render plain: "Not found", status: :not_found unless result&.log_path
36
+ return render plain: "Log not ready", status: :not_found unless File.exist?(result.log_path)
37
+
38
+ send_file result.log_path, type: "text/plain", disposition: "attachment"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,89 @@
1
+ module Devformance
2
+ class PlaygroundController < ApplicationController
3
+ def run
4
+ query_string = params[:query]
5
+
6
+ unless query_string.present?
7
+ return render json: { status: "error", output: "Empty query" }, status: :unprocessable_entity
8
+ end
9
+
10
+ result = nil
11
+ duration = 0
12
+ slow_queries_detected = []
13
+
14
+ begin
15
+ # Enable bullet specifically for this block if possible, or track via notifications
16
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |name, start, finish, id, payload|
17
+ # Only log SELECT/UPDATE/INSERT/DELETE that aren't schema
18
+ if payload[:sql] !~ /SCHEMA/
19
+ Rails.logger.debug "SQL: #{payload[:sql]}"
20
+ end
21
+ end
22
+
23
+ Bullet.start_request if defined?(Bullet)
24
+
25
+ duration = Benchmark.ms do
26
+ # Evaluate safely (MVP risk assumed acceptable for demo playground)
27
+ # Using a restricted binding or at least catching standard errors
28
+ # In a real app we'd build an AST or not allow eval, but for this devformance demo it's requested
29
+ result = eval(query_string)
30
+
31
+ # Force execution if it's an ActiveRecord::Relation
32
+ result = result.to_a if result.is_a?(ActiveRecord::Relation)
33
+ end
34
+
35
+ if defined?(Bullet) && Bullet.notification_collector.notifications_present?
36
+ Bullet.notification_collector.collection.each do |notification|
37
+ next unless notification.is_a?(Bullet::Notification::NPlusOneQuery)
38
+
39
+ # We parse out the model and association
40
+ model = notification.base_class rescue "Unknown"
41
+ suggestion = notification.body
42
+
43
+ sq = Devformance::SlowQuery.create!(
44
+ model_class: model,
45
+ line_number: caller.first.match(/:(\d+):/)&.captures&.first&.to_i || 0,
46
+ fix_suggestion: suggestion
47
+ )
48
+
49
+ slow_queries_detected << sq
50
+
51
+ ActionCable.server.broadcast("devformance:metrics", {
52
+ type: "new_slow_query",
53
+ payload: {
54
+ id: sq.id,
55
+ model_class: sq.model_class,
56
+ line_number: sq.line_number,
57
+ fix_suggestion: sq.fix_suggestion,
58
+ duration: duration.round(2)
59
+ }
60
+ })
61
+ end
62
+ end
63
+
64
+ Bullet.end_request if defined?(Bullet)
65
+
66
+ QueryLog.create(query: query_string, duration: duration) rescue nil
67
+
68
+ render json: {
69
+ status: "success",
70
+ duration: duration.round(2),
71
+ output: result.inspect.truncate(1000)
72
+ }
73
+ rescue Exception => e
74
+ Bullet.end_request if defined?(Bullet)
75
+ # Also log failed queries
76
+ QueryLog.create(query: query_string, duration: 0) rescue nil
77
+
78
+ render json: {
79
+ status: "error",
80
+ duration: duration.round(2),
81
+ output: "#{e.class}: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
82
+ }
83
+ ensure
84
+ # Unsubscribe if we did
85
+ ActiveSupport::Notifications.unsubscribe("sql.active_record") rescue nil
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ module ApplicationHelper
2
+ def icon_svg_path
3
+ Devformance::Engine.routes.url_helpers.icon_svg_path
4
+ end
5
+
6
+ def icon_png_path
7
+ Devformance::Engine.routes.url_helpers.icon_png_path
8
+ end
9
+ end