rails_pulse 0.3.3 → 0.4.0.pre.1

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +272 -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/templates/db/rails_pulse_schema.rb +54 -6
  127. data/lib/generators/rails_pulse/templates/rails_pulse.rb +152 -16
  128. data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
  129. data/lib/rails_pulse/cleanup_service.rb +110 -15
  130. data/lib/rails_pulse/configuration.rb +125 -6
  131. data/lib/rails_pulse/engine.rb +32 -17
  132. data/lib/rails_pulse/installers/config_updater.rb +202 -0
  133. data/lib/rails_pulse/job_run_collector.rb +43 -16
  134. data/lib/rails_pulse/middleware/asset_server.rb +11 -0
  135. data/lib/rails_pulse/middleware/request_collector.rb +9 -4
  136. data/lib/rails_pulse/packaged_assets.rb +147 -0
  137. data/lib/rails_pulse/route_indexes.rb +66 -0
  138. data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
  139. data/lib/rails_pulse/tracker.rb +12 -9
  140. data/lib/rails_pulse/version.rb +1 -1
  141. data/lib/rails_pulse.rb +1 -0
  142. data/lib/rails_pulse_server.ru +16 -47
  143. data/lib/tasks/rails_pulse.rake +83 -4
  144. data/lib/tasks/rails_pulse_assets.rake +15 -0
  145. data/lib/tasks/rails_pulse_tasks.rake +29 -0
  146. data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
  147. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  148. data/public/rails-pulse-assets/rails-pulse.js +18 -117
  149. metadata +78 -37
  150. data/Rakefile +0 -554
  151. data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
  152. data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
  153. data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
  154. data/app/javascript/rails_pulse/theme.js +0 -416
  155. data/config/brakeman.ignore +0 -190
  156. data/config/brakeman.yml +0 -68
  157. data/config/initializers/rails_pulse.rb +0 -287
  158. data/public/rails-pulse-assets/csp-test.js +0 -120
  159. data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
  160. data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
  161. data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
  162. data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
  163. data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
  164. data/vendor/assets/javascripts/rails-pulse.js +0 -157
  165. data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
  166. data/vendor/assets/stylesheets/rails-pulse.css +0 -1
  167. data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36c07de8920dddd7bb85571c3c01b94c197866dfeb45c471cad71dc829c61601
4
- data.tar.gz: c4cfbade2a92decf14272c76b32028918dab1bdc60d61bf95e9b6a9b69b0e07b
3
+ metadata.gz: 19b495bf5ea042672667ee099947d337731557f880af30f7d2dc26f507cbea0f
4
+ data.tar.gz: cb8946047ebb55a662742d42d143f47b5b3f3b982779fb289e4abfe418ac1c59
5
5
  SHA512:
6
- metadata.gz: 65565ffc47a14f6b4402dd3c83bddd8c422732460b91bf4a07655cc9de034d8535e0ccc679bbeb83e33ca83694d2891868dc60ee2e90243f20c0503287bba8b2
7
- data.tar.gz: b2b2c03e714bd1081ee152aadcc72f892b5a5927d2823725b10eb4ddaebaceedc5216ac48fbfe56372cb77f9b3701645f9eea27c62d400ad39ed4bef0840717c
6
+ metadata.gz: 428db96f32b2624638e99aacad09ef5dbeac23b6e11df316f7db66757c997777e846907830993657f4c76c0ea81dbfc92246ad93e1f5f3cc1c5b53ca80cb6065
7
+ data.tar.gz: 3f73d50c25352fc8a3dbaabe8cee59a51624fbebb1d23dfc58208bf42add67a33a56003d98769610559cd8211f8dab76a7a478da6cac0883f7a0d0e6d07f6ebb
data/CHANGELOG.md ADDED
@@ -0,0 +1,272 @@
1
+ # Changelog
2
+
3
+ All notable changes to Rails Pulse will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0.pre.1] - 2026-09-03
11
+
12
+ This release contains a **breaking schema change** and requires a one-time data
13
+ migration. Read "Upgrading from 0.3.x" below before deploying. Take a database
14
+ backup first: the route migration is irreversible.
15
+
16
+ ### Security
17
+
18
+ - **Job failure messages are redacted and bounded.** `rails_pulse_job_runs.error_message`
19
+ used to be stored raw and unbounded. It now goes through the same sanitizer as
20
+ exception messages: `key=value` / `key: value` / `"key":"value"` fragments are
21
+ masked whenever the key matches `config.filter_parameters` (same rules as logged
22
+ params), and MySQL `Duplicate entry '<value>'` values are redacted alongside
23
+ PostgreSQL `DETAIL:` clauses.
24
+ - **CSRF protection is now declared by the engine.** `RailsPulse::ApplicationController`
25
+ calls `protect_from_forgery with: :exception` itself instead of inheriting it from the
26
+ host's `default_protect_from_forgery`, which only `load_defaults 5.2+` enables. Hosts
27
+ on an older `load_defaults` had no CSRF protection on any engine endpoint (tags,
28
+ exception status, query reanalyze, session filters). Brakeman's `CheckForgerySetting`
29
+ is no longer skipped.
30
+ - **Authentication hooks fail closed.** An `authentication_method` that returned
31
+ `false` without rendering or redirecting used to grant access — `proc { current_user&.admin? }`
32
+ looked right and let every non-admin in. Literal `false` is now a 403; `nil`
33
+ (what the documented `unless … redirect_to … end` style returns on success) still allows.
34
+ - **EXPLAIN hardening.** The query analyzer now refuses SQL containing `;`, `--` or `/*`
35
+ anywhere (PostgreSQL's `execute` runs every statement in a string, so a captured
36
+ `SELECT …; UPDATE …` would have re-run the UPDATE on each page view), runs the
37
+ PostgreSQL EXPLAIN inside `SET LOCAL transaction_read_only = on` with a 5s
38
+ `statement_timeout` instead of `Timeout.timeout`, and always rolls its savepoint back.
39
+ - **Deployments API input is bounded.** `revision` is capped at 255 characters,
40
+ serialized `metadata` at 4 KB, and `started_at` may be at most one hour in the future
41
+ (a forged future marker would have stamped every later exception with a revision
42
+ that never shipped). `rails_pulse_deployments` now has a count cap
43
+ (`max_table_records`, default 1 000) enforced by `CleanupService`; deployments are
44
+ never pruned by age.
45
+ - **Backtrace source snippets are limited to `app/`, `lib/` and `config/routes.rb`**
46
+ (`.rb`, `.erb`, `.rake`, `.haml`, `.slim`, `.jbuilder`). Previously any file under
47
+ `Rails.root` was readable through a backtrace frame, including initializers with
48
+ inline keys, `config/database.yml`, and `.env`.
49
+ - **Standalone dashboard session cookie is `Secure` in production** (override with
50
+ `RAILS_PULSE_INSECURE_SESSION=1`). The rackup's "no Rails app" branch, which could
51
+ never boot (`NameError: uninitialized constant Rails`), is replaced with a clear
52
+ error; the deployment docs now give the working invocation.
53
+
54
+ ### Added
55
+
56
+ - **Exception tracking** — Captures unhandled exceptions from web requests and
57
+ background jobs, groups them by class and location, and displays backtraces (first
58
+ 50 frames) with filtered request params in a new Exceptions tab. Exception
59
+ messages are redacted before storage: `key=value` / `key: value` / `"key":"value"`
60
+ fragments are masked whenever the key matches `config.filter_parameters`, and
61
+ MySQL `Duplicate entry '<value>'` values are redacted alongside PostgreSQL
62
+ `DETAIL:` clauses.
63
+ - **`track_exceptions` config option** — Gem default is `false`, so existing installs
64
+ do not begin capturing on upgrade; the upgrade generator inserts `false`
65
+ explicitly. The install template sets `true` for new apps.
66
+ - **`capture_exception_params` config option** — Captures request params alongside
67
+ each occurrence, filtered via Rails' `filter_parameters`. Occurrences whose params
68
+ exceed 10 KB after filtering are stored without params. Default `true`, and only
69
+ consulted while `track_exceptions` is on.
70
+ - **`exception_message_filter` config option** — an optional `->(message, exception)`
71
+ hook applied after the built-in redaction for app-specific patterns. If the hook
72
+ raises, the message is stored as `[FILTERED]` rather than unfiltered.
73
+ - **`authorize` config option** — a fail-closed predicate, `->(controller) { … }`
74
+ (or a zero-argument proc run in the controller). Anything falsy is a 403. Runs after
75
+ `authentication_method` when both are set, and replaces the HTTP Basic fallback on
76
+ its own. This is now the recommended way to gate the dashboard.
77
+ - **Upgrade generator syncs new initializer settings** — Appends keys (and
78
+ `max_table_records` entries) the host file does not already mention, without
79
+ rewriting existing values. Review with `git diff`.
80
+
81
+ ### Changed
82
+
83
+ - **BREAKING — route identity is now `[controller_action, path]`.** GET `/users`
84
+ (`users#index`) and POST `/users` (`users#create`) are distinct routes. Dynamic
85
+ paths are normalized at capture time (`/posts/42` → `/posts/:id`).
86
+ - **A one-time backfill is mandatory.** `rails rails_pulse:migrate_routes` backfills
87
+ `controller_action` (from the live router, then from request history), collapses
88
+ historical literal paths, and merges routes sharing an action (GET+POST `/sign_in`
89
+ → `sessions#new`). A schema migrate alone leaves the Action column empty. The
90
+ upgrade generator and the dashboard both warn when the step is outstanding.
91
+ - **The JavaScript bundle is 66% smaller: 2.19 MB → 759 KB (754 KB → 248 KB gzipped).**
92
+ ECharts is now tree-shaken — `application.js` imports from `echarts/core` and
93
+ registers only the series and components the dashboard uses, instead of the
94
+ `echarts` barrel that bundled every chart type including maps, sankey and gauge.
95
+ Charts outside that set must now register their module explicitly; see
96
+ `docs/stimulus_echarts_usage.md`. The stylesheet is 5% smaller.
97
+
98
+ ### Removed
99
+
100
+ - **BREAKING — `rails_pulse_routes.method` is dropped.** The HTTP verb now lives on
101
+ each request (`rails_pulse_requests.method`); a route carries an `http_methods`
102
+ array instead. All application, worker, and dashboard processes must be restarted
103
+ together against the migrated schema. A rolling restart that leaves 0.3.x
104
+ processes running will silently drop request tracking and 500 the routes dashboard.
105
+ - **BREAKING — the route migration is irreversible.** `db:rollback` raises
106
+ `ActiveRecord::IrreversibleMigration` on
107
+ `ChangeRailsPulseRoutesToMultiVerbModel`, and routes merged by
108
+ `rails_pulse:migrate_routes` are deleted with no audit trail. Recovery from a bad
109
+ upgrade is restore-from-backup only.
110
+ - **Three unreachable Stimulus controllers** — `form`, `timezone` and
111
+ `period_selector` — were bundled and registered despite no view referencing them.
112
+ `timezone` also ran a `MutationObserver` over the whole document body.
113
+ - **`theme.js`**, which registered a chart theme that `application.js` immediately
114
+ overwrote. Its UMD wrapper called `require('echarts')`, which alone would have
115
+ defeated the tree-shaking above.
116
+ - **Dead CSS**: unused css-zero ports, the removed dashboard period selector's styles,
117
+ and the disabled `toolbox` chart option.
118
+ - **`csp-test.js` is no longer shipped in the gem.** Its page is mounted only when
119
+ `Rails.env.local?`, so it was unreachable in a published gem. It remains in the
120
+ repository for development and the CSP compliance system test.
121
+
122
+ ### Fixed
123
+
124
+ - **Tag filters work on SQLite for tags containing `_`.** The `LIKE` patterns were
125
+ escaped but issued without an `ESCAPE` clause; SQLite has no default escape
126
+ character, so `my_tag` never matched and "hide records tagged my_tag" hid nothing.
127
+ All three adapters now get an explicit `ESCAPE '!'`.
128
+ - **Hand-edited query strings no longer 500.** `?q=string`, unparseable custom date
129
+ ranges, unparseable `occurred_at` filters, a non-array `enabled_tags`, and
130
+ oversized or unparseable session time filters all fall back to defaults instead of
131
+ raising. Session-stored presets and performance thresholds are validated against
132
+ the known values.
133
+ - **Chart click/zoom got slower the more you switched chart tabs.** The requests,
134
+ routes and queries index pages attached a fresh `document` mouseup listener every
135
+ time a chart tab was re-selected, and only the most recent one was ever removed.
136
+ After N switches a single click anywhere on the page re-ran the zoom handler N
137
+ times. The listener is now bound once per controller.
138
+ - **Zooming onto the first column of a category chart reset the range.** Visible-range
139
+ extraction used `dataZoom.startValue || 0`, so a legitimate index of `0` fell through
140
+ to the full-axis fallback.
141
+ - **Hover popovers could throw after a table refresh.** `popover_controller` left its
142
+ show/hide timers running on disconnect, so a pending 700ms open could fire against
143
+ an element a fetch had already replaced.
144
+ - **The time range selector's hover border was invisible.** It referenced
145
+ `--color-border-emphasis`, which is defined nowhere, so the declaration was invalid
146
+ at computed-value time and the border fell back to `currentColor`. Its light-mode
147
+ rules also never applied on a first visit, because `data-color-scheme` is only set
148
+ after an explicit toggle.
149
+ - **`[popover].positioned` was declared twice** at equal specificity, so correct
150
+ popover placement depended on CSS file load order. Merged into one rule.
151
+ - Fixed upgrading when using a separate database installation.
152
+ - Separate-database installs set `schema_dump: false`, so `db:migrate` no longer
153
+ dumps or loads `db/rails_pulse_structure.sql` (#189).
154
+ - Fixed `assets:precompile` failing on memory-constrained hosts by not registering
155
+ dashboard assets with the host Sprockets pipeline. Precompile copies the pre-built
156
+ files into `public/assets` (digested, no compressor) so `config.asset_host` and
157
+ CDN-only CSP work; development and hosts without a pipeline still use
158
+ `/rails-pulse-assets/<gem-version>/...`.
159
+ - SQLite `schema.rb` now keeps the partial unique index on unrecognised routes
160
+ (`WHERE controller_action IS NULL`). A raw `CREATE INDEX` was dumped without the
161
+ predicate, so `db:schema:load` and parallel tests unique-constrained every path.
162
+
163
+ ### Upgrading from 0.3.x
164
+
165
+ Applies to every 0.3.x release (0.3.0 through 0.3.3). **Back up your database first.**
166
+
167
+ ```bash
168
+ bundle update rails_pulse
169
+ rails generate rails_pulse:upgrade
170
+ rails db:migrate # separate Pulse database: rails db:migrate:rails_pulse
171
+ rails rails_pulse:migrate_routes # required — schema migrate alone leaves Action empty
172
+ ```
173
+
174
+ Then restart **all** processes together, not as a rolling deploy.
175
+
176
+ Separate-database hosts: add `schema_dump: false` to the `rails_pulse` entry in
177
+ `config/database.yml` and delete `db/rails_pulse_structure.sql` if it exists. Do not
178
+ run `db:setup` / `db:prepare` as a substitute for `db:migrate:rails_pulse`.
179
+
180
+ Exception tracking stays off after upgrading. Set `config.track_exceptions = true`
181
+ once you have reviewed what is captured.
182
+
183
+ ## [0.3.3] - 2026-06-23
184
+
185
+ - **Deployment tracking** — Record deployments via `POST /rails_pulse/deployments` or `rake rails_pulse:record_deployment[sha]`. Deployments appear as vertical marker lines on performance charts so you can correlate releases with regressions
186
+ - **`deployment_api_token` config option** — Secures the deployments endpoint with a token header for CI/CD use
187
+ - All multi-series charts now use a native ECharts time axis (`[timestamp_ms, value]` pairs) instead of a separate labels array, enabling deployment markers and better zoom behaviour
188
+
189
+ ## [0.3.0] - 2026-04-30
190
+
191
+ This is the largest release to date — a full UI overhaul across every section of
192
+ the dashboard. Charts are now switchable, the dashboard surfaces health status
193
+ automatically, and the underlying controller and model code has been substantially
194
+ refactored. Existing installs need to run the two new migrations. Hopefully future
195
+ releases will be smaller and more incremental than this one.
196
+
197
+ ### Added
198
+
199
+ - **New chart types across all sections** — Routes, queries, and jobs now have dedicated chart panels with switchable chart types (response time percentiles, request volume, error rate, execution volume, duration, failure rate)
200
+ - **Dashboard health summary** — New `HealthSummary` model surfaces an overall health status and highlights routes/queries/jobs that need attention
201
+ - **Dashboard "Needs Attention" section** — Automatically surfaces slow routes, high error rates, and problematic jobs without manual digging
202
+ - **Storage pressure indicator** — Tracks and displays database storage growth so you know when to adjust retention settings
203
+ - **Flame graph view for requests** — Request detail pages now include a flame graph visualisation of operation timing
204
+ - **P95 duration tracking for jobs** — Job runs now record and display p95 duration alongside average duration
205
+ - **Database load metric for queries** — New card and chart tracking cumulative database load (execution count × avg duration) over time
206
+ - **Diagnostic fields for queries** — Query show pages now surface diagnostic information alongside existing analysis
207
+ - **Chart series toggle** — Show/hide individual series on charts without leaving the page
208
+ - **Performance status concern** — Shared `HasPerformanceStatus` concern for models that report a health status
209
+ - **Metric strip component** — New compact summary strip component for displaying multiple metrics inline
210
+ - **Setup banner** — Onboarding banner shown to users who haven't completed setup
211
+ - **Time range selector** — Redesigned time range UI with a custom date range option
212
+ - **Suggestions service** — Query show pages surface optimisation suggestions (caching, controller, SQL, HTTP, view) via dedicated suggestion services
213
+ - **Statistics module** — New `RailsPulse::Statistics` module for shared statistical calculations
214
+ - **Cleanup stats reporter** — Detailed reporting on what the cleanup job removed each run
215
+ - **Cleanup task runner** — Extracted cleanup orchestration into `CleanupTaskRunner` for testability
216
+ - **Config and migration installers** — Extracted install/upgrade logic into `ConfigInstaller` and `MigrationInstaller` classes
217
+ - **Schema parser** — New `SchemaParser` for reading and diffing schema state during upgrades
218
+ - **Icon helper** — Centralised `IconHelper` for rendering SVG icons
219
+ - **Route helper** — Centralised `RouteHelper` for building internal dashboard links
220
+ - **CSP helper** — Dedicated `CspHelper` for content security policy nonce management
221
+ - **Backfill summaries job** — New job to backfill summary records for historical data
222
+
223
+ ### Changed
224
+
225
+ - **Dashboard redesigned** — Full dashboard overhaul with health summary, attention sections, and chart panels replacing the previous card layout
226
+ - **Routes, queries, and jobs index pages redesigned** — Consistent layout with metric cards, switchable chart tabs, and paginated tables
227
+ - **Metric card component updated** — Cards now support a richer data structure with trend indicators
228
+ - **Application controller refactored** — Controller concerns split into focused modules (`ChartTableConcern`, `MetricCardConcern`, `PaginationConcern`, `SessionFiltersConcern`, `TagFilterConcern`, `TimeRangeConcern`, `ZoomRangeConcern`)
229
+ - **`normalized_sql` column expanded to `text`** — Removes the 1000-character limit on PostgreSQL and MySQL (migration provided for existing installs)
230
+ - **Diagnostic fields added to queries table** — New columns captured at instrumentation time
231
+ - **Summary job and service refactored** — Cleaner separation between job scheduling and summary calculation logic
232
+ - **Cleanup job refactored** — Now uses `CleanupTaskRunner` and reports statistics via `CleanupStatsReporter`
233
+ - **Configuration expanded** — New configuration options exposed in the initializer template
234
+ - **Upgrade generator simplified** — Generator now delegates to `MigrationInstaller` and `ConfigInstaller`
235
+ - **Asset server middleware updated** — Improved asset serving reliability
236
+ - **Request collector middleware updated** — Performance improvements and cleaner instrumentation
237
+ - **Operation subscriber refactored** — Cleaner event handling and reduced complexity
238
+ - **Seeds refactored** — Dummy app seeds split into focused files under `db/seeds/rails_pulse/`
239
+ - **Test coverage significantly expanded** — Controller concerns, helpers, models, services, and system tests all substantially extended
240
+
241
+ ### Removed
242
+
243
+ - `StatusHelper` removed — status rendering consolidated into model concerns and view components
244
+ - `ChartFormatters` helper removed — chart formatting moved into chart model classes
245
+ - `chart_formatters_test.rb` removed alongside the helper
246
+ - Slow queries and slow routes table models removed — replaced by the Needs Attention system
247
+ - Unused `average_response_time` and `p95_response_time` dashboard chart classes removed
248
+ - `requests/tables/index.rb` and related test removed — requests index now uses a shared approach
249
+ - `routes/cards/average_response_times` and `routes/charts/average_response_times` removed — replaced by percentile-based equivalents
250
+ - `jobs/cards/average_duration` and `jobs/cards/total_jobs` removed — replaced by updated cards
251
+ - Timezone controller removed
252
+ - Application mailer stub removed
253
+
254
+ ## [0.2.7] - 2026-04-17
255
+
256
+ No changelog entry — see git history.
257
+
258
+ ## [0.2.6] - 2026-04-15
259
+
260
+ No changelog entry — see git history.
261
+
262
+ ## [0.2.5] - 2026-04-14
263
+
264
+ No changelog entry — see git history.
265
+
266
+ [Unreleased]: https://github.com/railspulse/rails_pulse/compare/v0.4.0.pre.1...HEAD
267
+ [0.4.0.pre.1]: https://github.com/railspulse/rails_pulse/compare/v0.3.3...v0.4.0.pre.1
268
+ [0.3.3]: https://github.com/railspulse/rails_pulse/compare/v0.3.2...v0.3.3
269
+ [0.3.0]: https://github.com/railspulse/rails_pulse/compare/v0.2.7...v0.3.0
270
+ [0.2.7]: https://github.com/railspulse/rails_pulse/compare/v0.2.6...v0.2.7
271
+ [0.2.6]: https://github.com/railspulse/rails_pulse/compare/v0.2.5...v0.2.6
272
+ [0.2.5]: https://github.com/railspulse/rails_pulse/releases/tag/v0.2.5
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  ![License](https://img.shields.io/badge/License-MIT-green)
14
14
  ![Ruby Version](https://img.shields.io/badge/Ruby-3.0%2B-red)
15
15
 
16
- Rails Pulse is a Rails engine that monitors your app's performance from the inside. It tracks slow requests, N+1 queries, SQL performance, and background jobs. All data stays in your own database, no third-party cloud, no SaaS subscription, no data leaving your servers.
16
+ Rails Pulse is a Rails engine that monitors your app's performance from the inside. It tracks slow requests, N+1 queries, SQL performance, background jobs, and unhandled exceptions. All data stays in your own database, no third-party cloud, no SaaS subscription, no data leaving your servers.
17
17
 
18
18
  <table border="0">
19
19
  <tr border="0" style="border:0">
@@ -60,6 +60,32 @@ RailsPulse::CleanupJob.perform_later # cron: 0 1 * * *
60
60
 
61
61
  Your dashboard is now at `http://localhost:3000/rails_pulse`.
62
62
 
63
+ ## Upgrading
64
+
65
+ From 0.3.3:
66
+
67
+ ```bash
68
+ bundle update rails_pulse
69
+ rails generate rails_pulse:upgrade
70
+ rails db:migrate # or: rails db:migrate:rails_pulse
71
+ rails rails_pulse:migrate_routes # required — fills Action and merges same-action paths
72
+ ```
73
+
74
+ Restart all processes after migrate. This release changes how routes are stored (`method` moves off the route onto each request), so mixed old/new processes are not supported.
75
+
76
+ The upgrade generator appends new settings to `config/initializers/rails_pulse.rb` without rewriting what you already set. Review with `git diff` and keep or discard hunks.
77
+
78
+ Exception tracking is **off** for existing installs. The generator inserts `config.track_exceptions = false`; set it to `true` after migrating to opt in:
79
+
80
+ ```ruby
81
+ config.track_exceptions = true
82
+ config.capture_exception_params = true # params are filtered via Rails' filter_parameters
83
+ ```
84
+
85
+ Separate-database hosts: set `schema_dump: false` on the `rails_pulse` entry in `config/database.yml` and delete `db/rails_pulse_structure.sql` if that file exists.
86
+
87
+ If you previously added `rails-pulse.js` / `rails-pulse.css` to `config.assets.precompile`, remove those entries — the gem no longer registers dashboard assets with Sprockets (that re-minify OOMs small hosts). Production deploys that use `config.asset_host` or a CDN-only CSP should run `assets:precompile` so `rails_pulse:install_assets` copies digested files into `public/assets`.
88
+
63
89
  Full install guide: [railspulse.com/documentation/installation](https://railspulse.com/documentation/installation)
64
90
 
65
91
  Separate database setup: [railspulse.com/documentation/database](https://railspulse.com/documentation/database)
@@ -81,6 +107,9 @@ RailsPulse.configure do |config|
81
107
  config.track_jobs = true
82
108
  config.capture_job_arguments = false # keep false to protect sensitive data
83
109
 
110
+ config.track_exceptions = true
111
+ config.capture_exception_params = true # params are filtered via Rails' filter_parameters
112
+
84
113
  config.full_retention_period = 30.days
85
114
  end
86
115
  ```
@@ -89,11 +118,20 @@ Full configuration reference: [railspulse.com/documentation/advanced](https://ra
89
118
 
90
119
  ## Authentication
91
120
 
92
- Rails Pulse has no built-in authentication, you protect the dashboard using your app's existing auth. Add an `authentication_method` to the initializer:
121
+ Rails Pulse has no built-in user accounts; you protect the dashboard using your app's existing auth. Authentication is on by default outside development and test, and with nothing configured it falls back to HTTP Basic against `RAILS_PULSE_USERNAME` / `RAILS_PULSE_PASSWORD` (denying everything if the password is unset).
122
+
123
+ The simplest hook is a predicate that receives the controller and returns `true` to allow — anything else is a 403:
124
+
125
+ ```ruby
126
+ RailsPulse.configure do |config|
127
+ config.authorize = ->(controller) { controller.current_user&.admin? }
128
+ end
129
+ ```
130
+
131
+ If you need to redirect to a login page instead, use `authentication_method`, which runs inside the controller and denies by rendering or redirecting:
93
132
 
94
133
  ```ruby
95
134
  RailsPulse.configure do |config|
96
- config.authentication_enabled = true
97
135
  config.authentication_redirect_path = "/login"
98
136
 
99
137
  config.authentication_method = proc {
@@ -104,6 +142,8 @@ RailsPulse.configure do |config|
104
142
  end
105
143
  ```
106
144
 
145
+ Returning `false` from `authentication_method` without responding is also treated as a denial, but `nil` (what `unless … end` returns on success) allows the request — so keep predicate-style checks in `authorize`.
146
+
107
147
  Authentication guide: [railspulse.com/documentation/authentication](https://railspulse.com/documentation/authentication)
108
148
 
109
149
  ## Features
@@ -111,6 +151,7 @@ Authentication guide: [railspulse.com/documentation/authentication](https://rail
111
151
  - **Request monitoring** — every request is timed and stored with its route, status, SQL count, and duration. Slow requests are flagged automatically based on thresholds you control.
112
152
  - **Query analysis** — captures the queries behind each request, detects N+1 patterns, and tracks normalized SQL across requests so you can see which queries are hurting you in production, not just in development.
113
153
  - **Job tracking** — monitors background job duration, queue wait time, and failure rates. Works with any Active Job adapter.
154
+ - **Exception tracking** — captures unhandled exceptions from web requests and background jobs, groups them by class and location, and shows full backtraces with filtered request params. See recurring errors in production without a separate error monitoring service.
114
155
  - **System health bar** — at-a-glance dashboard summary showing healthy, slow, and critical counts across your routes, queries, jobs, and storage. Lets you see the overall state of your app before drilling into specifics.
115
156
  - **No data leaves your app** — everything is stored in your own database. No third-party cloud, no SaaS subscription, no outbound connections.
116
157
  - **Low overhead** — tracking is async and uses a thread-local flag to skip recording Rails Pulse's own internal requests.
@@ -41,6 +41,27 @@ html[data-color-scheme="dark"] .badge--negative-inverse {
41
41
  --badge-background: rgb(47, 47, 47);
42
42
  }
43
43
 
44
+ html[data-color-scheme="dark"] .badge--get {
45
+ --badge-background: var(--blue-950);
46
+ --badge-color: var(--blue-200);
47
+ }
48
+
49
+ html[data-color-scheme="dark"] .badge--post {
50
+ --badge-background: var(--green-950);
51
+ --badge-color: var(--green-200);
52
+ }
53
+
54
+ html[data-color-scheme="dark"] .badge--put,
55
+ html[data-color-scheme="dark"] .badge--patch {
56
+ --badge-background: var(--amber-950);
57
+ --badge-color: var(--amber-200);
58
+ }
59
+
60
+ html[data-color-scheme="dark"] .badge--delete {
61
+ --badge-background: var(--red-950);
62
+ --badge-color: var(--red-200);
63
+ }
64
+
44
65
  html[data-color-scheme="dark"] .input {
45
66
  --input-background: #535252;
46
67
  --input-border-color: #7e7d7d;
@@ -161,19 +182,6 @@ html[data-color-scheme="dark"] .input {
161
182
  z-index: 10;
162
183
  }
163
184
 
164
- .flame-bar-label {
165
- font-size: 11px;
166
- white-space: nowrap;
167
- overflow: hidden;
168
- text-overflow: ellipsis;
169
- padding: 0 5px;
170
- line-height: 20px;
171
- color: rgba(255, 255, 255, 0.92);
172
- display: block;
173
- pointer-events: none;
174
- user-select: none;
175
- }
176
-
177
185
  .flame-time-axis {
178
186
  position: relative;
179
187
  height: 20px;
@@ -232,22 +240,3 @@ html[data-color-scheme="dark"] .input {
232
240
  font-size: 11px;
233
241
  }
234
242
 
235
- /* REQUEST OPERATIONS BAR */
236
- .bar-container {
237
- height:10px;
238
- position:relative
239
- }
240
- .bar {
241
- background-color:#727579;
242
- height:100%;
243
- position:absolute;
244
- top:0
245
- }
246
- .bar:first-child {
247
- border-bottom-left-radius:1px;
248
- border-top-left-radius:1px
249
- }
250
- .bar:last-child {
251
- border-bottom-right-radius:1px;
252
- border-top-right-radius:1px
253
- }
@@ -0,0 +1,238 @@
1
+ .backtrace {
2
+ list-style: none;
3
+ margin: 0;
4
+ padding: 0;
5
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
6
+ font-size: 12px;
7
+ border-radius: 6px;
8
+ overflow: hidden;
9
+ }
10
+
11
+ .backtrace__frame {
12
+ display: flex;
13
+ flex-direction: column;
14
+ border-bottom: 1px solid var(--color-border-light);
15
+ }
16
+
17
+ .backtrace__frame:last-child {
18
+ border-bottom: none;
19
+ }
20
+
21
+ .backtrace__row {
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ gap: 8px;
26
+ padding: 7px 12px 7px 0;
27
+ }
28
+
29
+ /* App frames: highlighted with a coloured left accent */
30
+ .backtrace__frame--app {
31
+ background-color: rgba(59, 130, 246, 0.08);
32
+ border-left: 3px solid var(--blue-400, #3b82f6);
33
+ }
34
+
35
+ /* Gem/stdlib frames: muted */
36
+ .backtrace__frame--gem {
37
+ background-color: var(--color-surface);
38
+ border-left: 3px solid transparent;
39
+ opacity: 0.75;
40
+ }
41
+
42
+ .backtrace__frame--gem:hover {
43
+ opacity: 1;
44
+ }
45
+
46
+ .backtrace__location {
47
+ display: flex;
48
+ align-items: baseline;
49
+ gap: 0;
50
+ flex: 1;
51
+ min-width: 0;
52
+ overflow: hidden;
53
+ }
54
+
55
+ .backtrace__path {
56
+ color: var(--color-text-subtle);
57
+ white-space: nowrap;
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ flex-shrink: 1;
61
+ min-width: 0;
62
+ }
63
+
64
+ .backtrace__file {
65
+ color: var(--color-text);
66
+ font-weight: 700;
67
+ white-space: nowrap;
68
+ flex-shrink: 0;
69
+ }
70
+
71
+ .backtrace__method {
72
+ flex-shrink: 0;
73
+ color: var(--color-text-subtle);
74
+ white-space: nowrap;
75
+ font-size: 11px;
76
+ }
77
+
78
+ .backtrace__method span {
79
+ color: var(--color-text);
80
+ font-weight: 500;
81
+ }
82
+
83
+ /* Source expand toggle — only the chevron is interactive */
84
+
85
+ .backtrace__source-details {
86
+ flex-shrink: 0;
87
+ line-height: 1;
88
+ }
89
+
90
+ .backtrace__toggle-spacer {
91
+ flex-shrink: 0;
92
+ width: 40px;
93
+ }
94
+
95
+ .backtrace__toggle {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 40px;
100
+ height: 40px;
101
+ cursor: pointer;
102
+ list-style: none;
103
+ color: var(--color-text-subtle);
104
+ font-size: 32px;
105
+ border-radius: 3px;
106
+ transition: background-color 0.1s ease, color 0.1s ease;
107
+ user-select: none;
108
+ }
109
+
110
+ .backtrace__toggle::-webkit-details-marker {
111
+ display: none;
112
+ }
113
+
114
+ .backtrace__toggle::after {
115
+ content: "›";
116
+ display: block;
117
+ transition: transform 0.15s ease;
118
+ }
119
+
120
+ .backtrace__toggle:hover {
121
+ background-color: var(--color-bg-hover, rgba(0,0,0,0.06));
122
+ color: var(--color-text);
123
+ }
124
+
125
+ .backtrace__source-details[open] .backtrace__toggle::after {
126
+ transform: rotate(90deg);
127
+ }
128
+
129
+ /* Source code panel */
130
+
131
+ .backtrace__source {
132
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
133
+ font-size: 12px;
134
+ background-color: var(--color-bg-subtle, #f9fafb);
135
+ border-top: 1px solid var(--color-border-light);
136
+ overflow-x: auto;
137
+ }
138
+
139
+ .backtrace__source--hidden {
140
+ display: none;
141
+ }
142
+
143
+ .backtrace__frame:has(.backtrace__source-details[open]) .backtrace__source {
144
+ display: block;
145
+ }
146
+
147
+ .backtrace__source-line {
148
+ display: flex;
149
+ gap: 0;
150
+ line-height: 1.6;
151
+ white-space: pre;
152
+ }
153
+
154
+ .backtrace__source-line--current {
155
+ background-color: rgba(234, 179, 8, 0.15);
156
+ }
157
+
158
+ .backtrace__source-lineno {
159
+ flex-shrink: 0;
160
+ width: 44px;
161
+ padding: 0 12px 0 8px;
162
+ text-align: right;
163
+ color: var(--color-text-subtle);
164
+ border-right: 1px solid var(--color-border-light);
165
+ user-select: none;
166
+ }
167
+
168
+ .backtrace__source-line--current .backtrace__source-lineno {
169
+ color: rgba(161, 98, 7, 0.9);
170
+ background-color: rgba(234, 179, 8, 0.2);
171
+ font-weight: 700;
172
+ }
173
+
174
+ .backtrace__source-code {
175
+ padding: 0 12px;
176
+ color: var(--color-text);
177
+ flex: 1;
178
+ }
179
+
180
+ /* Request params */
181
+
182
+ .exception-params {
183
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
184
+ font-size: 12px;
185
+ border-radius: 6px;
186
+ overflow: hidden;
187
+ border: 1px solid var(--color-border-light);
188
+ }
189
+
190
+ .exception-params__row {
191
+ display: flex;
192
+ gap: 16px;
193
+ padding: 6px 12px;
194
+ border-bottom: 1px solid var(--color-border-light);
195
+ align-items: baseline;
196
+ }
197
+
198
+ .exception-params__row:last-child {
199
+ border-bottom: none;
200
+ }
201
+
202
+
203
+ .exception-params__key {
204
+ flex-shrink: 0;
205
+ width: 200px;
206
+ color: var(--color-text-subtle);
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ white-space: nowrap;
210
+ }
211
+
212
+ .exception-params__value {
213
+ color: var(--color-text);
214
+ word-break: break-all;
215
+ }
216
+
217
+ /* Dark mode */
218
+ html[data-color-scheme="dark"] .backtrace__frame--app {
219
+ background-color: rgba(59, 130, 246, 0.15);
220
+ border-left-color: var(--blue-400, #3b82f6);
221
+ }
222
+
223
+ html[data-color-scheme="dark"] .backtrace__frame--gem {
224
+ background-color: transparent;
225
+ }
226
+
227
+ html[data-color-scheme="dark"] .backtrace__source {
228
+ background-color: rgba(0, 0, 0, 0.3);
229
+ }
230
+
231
+ html[data-color-scheme="dark"] .backtrace__source-line--current {
232
+ background-color: rgba(234, 179, 8, 0.1);
233
+ }
234
+
235
+ html[data-color-scheme="dark"] .backtrace__source-line--current .backtrace__source-lineno {
236
+ color: rgba(253, 224, 71, 0.8);
237
+ background-color: rgba(234, 179, 8, 0.12);
238
+ }