coverband 6.2.1 → 7.0.0.rc.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/diagram.yml +2 -2
  3. data/.github/workflows/main.yml +43 -7
  4. data/.gitignore +2 -1
  5. data/Gemfile +15 -0
  6. data/Gemfile.rails7.0 +11 -0
  7. data/Gemfile.rails7.1 +5 -1
  8. data/Gemfile.rails7.2 +4 -0
  9. data/Gemfile.rails8.0 +14 -0
  10. data/README.md +277 -12
  11. data/Rakefile +25 -6
  12. data/changes.md +65 -0
  13. data/coverband.gemspec +17 -12
  14. data/lib/coverband/adapters/active_support_cache_store.rb +54 -0
  15. data/lib/coverband/adapters/base.rb +42 -10
  16. data/lib/coverband/adapters/file_store.rb +2 -2
  17. data/lib/coverband/adapters/hash_redis_store.rb +22 -3
  18. data/lib/coverband/adapters/null_store.rb +0 -4
  19. data/lib/coverband/adapters/redis_cleanup.rb +101 -0
  20. data/lib/coverband/adapters/redis_store.rb +32 -65
  21. data/lib/coverband/adapters/session_coverage.rb +172 -0
  22. data/lib/coverband/adapters/stdout_store.rb +0 -4
  23. data/lib/coverband/adapters/tracker_storage/base.rb +79 -0
  24. data/lib/coverband/adapters/tracker_storage/document_repository.rb +49 -0
  25. data/lib/coverband/adapters/tracker_storage/factory.rb +57 -0
  26. data/lib/coverband/adapters/tracker_storage/redis_hash_repository.rb +127 -0
  27. data/lib/coverband/collectors/abstract_tracker.rb +135 -51
  28. data/lib/coverband/collectors/delta.rb +21 -7
  29. data/lib/coverband/collectors/query_burst_tracker.rb +321 -0
  30. data/lib/coverband/collectors/route_tracker.rb +6 -0
  31. data/lib/coverband/collectors/tracker_registry.rb +63 -0
  32. data/lib/coverband/collectors/translation_tracker.rb +6 -0
  33. data/lib/coverband/collectors/view_tracker.rb +11 -5
  34. data/lib/coverband/configuration.rb +101 -126
  35. data/lib/coverband/mcp/http_handler.rb +1 -1
  36. data/lib/coverband/mcp/server.rb +4 -1
  37. data/lib/coverband/mcp/tools/get_storage_health.rb +28 -0
  38. data/lib/coverband/reporters/base.rb +0 -2
  39. data/lib/coverband/reporters/html_report.rb +2 -3
  40. data/lib/coverband/reporters/json_report.rb +2 -2
  41. data/lib/coverband/reporters/web.rb +97 -16
  42. data/lib/coverband/storage/document.rb +173 -0
  43. data/lib/coverband/storage/generation.rb +165 -0
  44. data/lib/coverband/storage/generation_change.rb +53 -0
  45. data/lib/coverband/storage/generation_coordinator.rb +145 -0
  46. data/lib/coverband/storage/io_guard.rb +29 -0
  47. data/lib/coverband/storage/read_fallback.rb +30 -0
  48. data/lib/coverband/storage/redis_target.rb +74 -0
  49. data/lib/coverband/storage/session.rb +527 -0
  50. data/lib/coverband/storage/target.rb +117 -0
  51. data/lib/coverband/storage/writer.rb +194 -0
  52. data/lib/coverband/storage_health.rb +90 -0
  53. data/lib/coverband/utils/configuration_template.rb +14 -1
  54. data/lib/coverband/utils/html_formatter.rb +3 -1
  55. data/lib/coverband/utils/railtie.rb +2 -2
  56. data/lib/coverband/utils/result.rb +5 -5
  57. data/lib/coverband/utils/results.rb +32 -16
  58. data/lib/coverband/utils/source_file.rb +17 -27
  59. data/lib/coverband/utils/tasks.rb +71 -6
  60. data/lib/coverband/version.rb +1 -1
  61. data/lib/coverband.rb +99 -20
  62. data/public/application.css +558 -137
  63. data/public/application.js +246 -0
  64. data/public/dead_code_session.js +173 -0
  65. data/test/benchmarks/benchmark.rake +31 -10
  66. data/test/coverband/adapters/active_support_cache_store_test.rb +613 -0
  67. data/test/coverband/adapters/capabilities_test.rb +80 -0
  68. data/test/coverband/adapters/redis_store_test.rb +63 -2
  69. data/test/coverband/adapters/solid_cache_store_test.rb +181 -0
  70. data/test/coverband/adapters/tracker_storage_test.rb +170 -0
  71. data/test/coverband/benchmarks/benchmark_memory_leak_check_test.rb +43 -0
  72. data/test/coverband/collectors/delta_test.rb +17 -0
  73. data/test/coverband/collectors/query_burst_tracker_test.rb +222 -0
  74. data/test/coverband/collectors/route_tracker_test.rb +2 -13
  75. data/test/coverband/collectors/tracker_registry_test.rb +218 -0
  76. data/test/coverband/collectors/translation_tracker_test.rb +76 -16
  77. data/test/coverband/collectors/view_tracker_test.rb +49 -12
  78. data/test/coverband/configuration_test.rb +36 -38
  79. data/test/coverband/mcp/server_test.rb +2 -1
  80. data/test/coverband/mcp/tools/get_storage_health_test.rb +41 -0
  81. data/test/coverband/reporters/html_test.rb +26 -0
  82. data/test/coverband/reporters/json_test.rb +53 -0
  83. data/test/coverband/reporters/web_test.rb +163 -0
  84. data/test/coverband/storage/generation_coordinator_test.rb +191 -0
  85. data/test/coverband/storage/io_guard_test.rb +127 -0
  86. data/test/coverband/storage/protocol_conformance_test.rb +725 -0
  87. data/test/coverband/storage_health_test.rb +99 -0
  88. data/test/coverband/track_key_test.rb +17 -0
  89. data/test/coverband/utils/cleanup_tasks_test.rb +188 -0
  90. data/test/coverband/utils/file_list_test.rb +2 -2
  91. data/test/coverband/utils/result_test.rb +4 -4
  92. data/test/coverband/utils/results_test.rb +100 -10
  93. data/test/coverband/utils/source_file_test.rb +40 -6
  94. data/test/forked/rails_cache_full_stack_test.rb +64 -0
  95. data/test/forked/rails_full_stack_test.rb +7 -0
  96. data/test/forked/rails_route_tracker_stack_test.rb +1 -1
  97. data/test/forked/rails_view_tracker_stack_test.rb +1 -1
  98. data/test/integration/full_stack_deferred_eager_test.rb +0 -1
  99. data/test/integration/full_stack_send_deferred_eager_test.rb +0 -1
  100. data/test/integration/full_stack_test.rb +0 -1
  101. data/test/integration/mcp_integration_test.rb +4 -2
  102. data/test/integration/standalone_json_report_test.rb +46 -0
  103. data/test/integration/without_redis_test.rb +76 -0
  104. data/test/javascript/dead_code_session.test.mjs +123 -0
  105. data/test/rails7_dummy/config/application.rb +1 -0
  106. data/test/rails7_dummy/config/coverband_missing_redis.rb +0 -2
  107. data/test/rails7_dummy/config/coverband_rails_cache.rb +16 -0
  108. data/test/rails8_dummy/config/application.rb +1 -0
  109. data/test/rails8_dummy/config/coverband_rails_cache.rb +3 -0
  110. data/test/rails_test_helper.rb +2 -1
  111. data/test/test_helper.rb +5 -17
  112. data/views/abstract_tracker.erb +49 -2
  113. data/views/file_list.erb +2 -2
  114. data/views/layout.erb +1 -0
  115. data/views/nav.erb +5 -0
  116. data/views/source_file.erb +4 -3
  117. metadata +56 -29
  118. data/lib/coverband/adapters/memcached_store.rb +0 -85
  119. data/lib/coverband/adapters/web_service_store.rb +0 -152
  120. data/lib/coverband/collectors/view_tracker_service.rb +0 -59
  121. data/test/coverband/adapters/memecached_store_test.rb +0 -26
  122. data/test/coverband/adapters/web_service_store_test.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c47e38934426a369c81dd7d08e5474ad0d8a784b541975c1aa7ee50fc9e2d1ce
4
- data.tar.gz: 346c8263e9c961631bcf4af3019d8473177088c5b9ae3cb3844dc7d86bcd7497
3
+ metadata.gz: 122ec261f93431af71ca87c0113241a9197af2daa07c3d32013b98d893cd3a78
4
+ data.tar.gz: 785fa634bdba056657cb2b0e358f387decb4aa020faee0f50580dedd929ddcaa
5
5
  SHA512:
6
- metadata.gz: 907a280ce513b23922b81c44674c135ab067e629ae9f8aaec6eb6a667946a52a85fd1e7a0fe17ee9b24c0a6b81b8b22bcb1e1c53e7ce75ccc7a9c3507b6dd183
7
- data.tar.gz: 60198f0536094c5505ea532b238d9d38359f936edb6ea21854c946d3a5f63e7e02b66f1240596b787cad898546a3d48d41595d5d4cab28bb09face2a4f405499
6
+ metadata.gz: 990b9e30e696866dd1e68e8c26b394608c3697f8564ea0ff2683592d1b02fabaae5a0dcc0aa4e3c7a12e73cef259aa2f441fd154a910cb7694c0d10acfb9dab7
7
+ data.tar.gz: 43c6e3982076da3771c7a961a9ac7c409c8576731d8e76b4a943a67323ff4e3391f9b96de89ba5050ee1c2868a436b25c9a386265d643bf82b053c6b65953f20
@@ -6,7 +6,7 @@ on:
6
6
  - main
7
7
  jobs:
8
8
  get_data:
9
- runs-on: ubuntu-latest
9
+ runs-on: ubuntu-24.04
10
10
  steps:
11
11
  - name: Checkout code
12
12
  uses: actions/checkout@master
@@ -14,4 +14,4 @@ jobs:
14
14
  uses: githubocto/repo-visualizer@main
15
15
  with:
16
16
  excluded_paths: "ignore,.github"
17
- branch: diagram
17
+ branch: diagram
@@ -13,8 +13,8 @@ jobs:
13
13
  fail-fast: false
14
14
  matrix:
15
15
  # need to figure out how to use redis on macos github actions
16
- # os: [ubuntu, macos]
17
- os: [ubuntu]
16
+ # os: [ubuntu-24.04, macos-15]
17
+ os: [ubuntu-24.04]
18
18
  # remove until I sort out CI issues for truffle
19
19
  # truffleruby,
20
20
  # truffleruby-head,
@@ -25,9 +25,11 @@ jobs:
25
25
  redis-version: [4, 5, 6, 7]
26
26
  exclude:
27
27
  # Rails 8 requires ruby 3.2+.
28
- - gemfile: 'rails_8.0'
28
+ - gemfile: 'Gemfile.rails8.0'
29
29
  ruby: '3.1'
30
- runs-on: ${{ matrix.os }}-latest
30
+ runs-on: ${{ matrix.os }}
31
+ env:
32
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
31
33
  services:
32
34
  redis:
33
35
  image: redis:${{ matrix.redis-version }}
@@ -38,20 +40,54 @@ jobs:
38
40
  --health-interval 10s
39
41
  --health-timeout 5s
40
42
  --health-retries 5
43
+ memcached:
44
+ image: memcached:1.6
45
+ ports:
46
+ - 11211:11211
47
+ options: >-
48
+ --health-cmd "timeout 2 bash -c '</dev/tcp/127.0.0.1/11211'"
49
+ --health-interval 10s
50
+ --health-timeout 5s
51
+ --health-retries 5
41
52
  steps:
42
- - uses: actions/checkout@v6
53
+ - uses: actions/checkout@v7
43
54
  - uses: ruby/setup-ruby@v1
44
55
  with:
45
56
  ruby-version: ${{ matrix.ruby }}
46
57
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
47
58
  - run: bundle exec rake test:all
59
+ env:
60
+ # the memcached adapter has an ENV gate so local runs need no service,
61
+ # which also meant CI never exercised it
62
+ COVERBAND_MEMCACHED: "true"
48
63
  - run: "RUBYOPT='--enable=frozen-string-literal --debug=frozen-string-literal' bundle exec rake"
64
+ env:
65
+ COVERBAND_MEMCACHED: "true"
66
+ # Solid Cache needs Rails 7.2+, and its engine changes process wide state,
67
+ # so it runs on its own, only where it is available.
68
+ #
69
+ # Its gems sit in an optional bundler group, which bundler-cache does not
70
+ # install, so the group is installed here rather than assumed present.
71
+ - name: Solid Cache adapter tests
72
+ if: matrix.gemfile == 'Gemfile.rails8.0'
73
+ run: |
74
+ bundle config set --local with solid_cache
75
+ bundle install --jobs 4
76
+ bundle exec rake test:solid_cache
49
77
  starndardrb:
50
- runs-on: ubuntu-latest
78
+ runs-on: ubuntu-24.04
51
79
  steps:
52
- - uses: actions/checkout@v6
80
+ - uses: actions/checkout@v7
53
81
  - uses: ruby/setup-ruby@v1
54
82
  with:
55
83
  ruby-version: 3.1
56
84
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
57
85
  - run: bundle exec standardrb --format github
86
+ javascript:
87
+ runs-on: ubuntu-24.04
88
+ steps:
89
+ - uses: actions/checkout@v7
90
+ - uses: actions/setup-node@v7
91
+ with:
92
+ node-version: "20"
93
+ - run: node --test test/javascript/*.test.mjs
data/.gitignore CHANGED
@@ -26,4 +26,5 @@ log/
26
26
  test/unique_files
27
27
  test/rails7_dummy/tmp
28
28
  CLAUDE.md
29
- .claude/
29
+ .claude/
30
+ scratch/
data/Gemfile CHANGED
@@ -11,3 +11,18 @@ gem "webrick"
11
11
 
12
12
  # Required for Ruby 3.4+ (extracted from stdlib)
13
13
  gem "cgi"
14
+
15
+ # ActiveSupport 8.1's MemCacheStore passes positional options to ConnectionPool,
16
+ # which connection_pool 3.x turned into keyword arguments. Pinned so the
17
+ # memcached backed cache adapter tests can actually run.
18
+ gem "connection_pool", "~> 2.5"
19
+
20
+ # Solid Cache is a central target for the ActiveSupport::Cache adapter: it is
21
+ # how Coverband reaches Postgres, MySQL, and SQLite. Needs Rails 7.2+.
22
+ #
23
+ # Kept out of the default group on purpose: the dummy Rails apps require every
24
+ # default gem, and Solid Cache's engine expects an ActiveJob they do not load.
25
+ group :solid_cache, optional: true do
26
+ gem "solid_cache"
27
+ gem "sqlite3"
28
+ end
data/Gemfile.rails7.0 CHANGED
@@ -9,5 +9,16 @@ gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
11
 
12
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
13
+ # which connection_pool 3.x turned into keyword arguments.
14
+ gem "connection_pool", "~> 2.5"
15
+
16
+ # Rack 2 requires ostruct, which Ruby 4 no longer ships as a default gem.
17
+ gem "ostruct"
18
+
12
19
  # Required for Ruby 3.4+ (extracted from stdlib)
13
20
  gem "cgi"
21
+
22
+ # railties 7.0 requires tsort without declaring it, and Ruby 4.1 no longer
23
+ # ships it as a default gem.
24
+ gem "tsort"
data/Gemfile.rails7.1 CHANGED
@@ -9,5 +9,9 @@ gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
11
 
12
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
13
+ # which connection_pool 3.x turned into keyword arguments.
14
+ gem "connection_pool", "~> 2.5"
15
+
12
16
  # Required for Ruby 3.4+ (extracted from stdlib)
13
- gem "cgi"
17
+ gem "cgi"
data/Gemfile.rails7.2 CHANGED
@@ -9,5 +9,9 @@ gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
11
 
12
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
13
+ # which connection_pool 3.x turned into keyword arguments.
14
+ gem "connection_pool", "~> 2.5"
15
+
12
16
  # Required for Ruby 3.4+ (extracted from stdlib)
13
17
  gem "cgi"
data/Gemfile.rails8.0 CHANGED
@@ -9,5 +9,19 @@ gem "haml"
9
9
  gem "slim"
10
10
  gem "webrick"
11
11
 
12
+ # ActiveSupport's MemCacheStore passes positional options to ConnectionPool,
13
+ # which connection_pool 3.x turned into keyword arguments.
14
+ gem "connection_pool", "~> 2.5"
15
+
12
16
  # Required for Ruby 3.4+ (extracted from stdlib)
13
17
  gem "cgi"
18
+
19
+ # Solid Cache is a central target for the ActiveSupport::Cache adapter: it is
20
+ # how Coverband reaches Postgres, MySQL, and SQLite. Needs Rails 7.2+.
21
+ #
22
+ # Kept out of the default group on purpose: the dummy Rails apps require every
23
+ # default gem, and Solid Cache's engine expects an ActiveJob they do not load.
24
+ group :solid_cache, optional: true do
25
+ gem "solid_cache"
26
+ gem "sqlite3"
27
+ end
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  # Coverband
4
4
 
5
5
  [![GithubCI](https://github.com/danmayer/coverband/workflows/CI/badge.svg)](https://github.com/danmayer/coverband/actions)
6
- [![Coverage Status](https://coveralls.io/repos/github/danmayer/coverband/badge.svg?branch=main)](https://coveralls.io/github/danmayer/coverband?branch=main)
7
6
  [![Maintainability](https://api.codeclimate.com/v1/badges/1e6682f9540d75f26da7/maintainability)](https://codeclimate.com/github/danmayer/coverband/maintainability)
8
7
  [![Discord Shield](https://img.shields.io/discord/609509533999562753)](https://discord.gg/KAH38EV)
9
8
 
@@ -69,12 +68,15 @@ The `rediss://` scheme works with:
69
68
 
70
69
  ## Gem Installation
71
70
 
72
- Add this line to your application's `Gemfile`, remember to `bundle install` after updating:
71
+ Add these lines to your application's `Gemfile`, remember to `bundle install` after updating:
73
72
 
74
- ```bash
73
+ ```ruby
75
74
  gem 'coverband'
75
+ gem 'redis' # the default store
76
76
  ```
77
77
 
78
+ Redis is the default store and needs no configuration beyond a reachable Redis. Since Coverband 7.0 the `redis` gem is no longer installed for you, so add it yourself as above. If you would rather keep coverage somewhere else (Solid Cache, memcached, files), leave out `redis` and set `config.store`, see [ActiveSupport::Cache Store](#activesupportcache-store). Without the `redis` gem and without a configured store, Coverband logs a warning at boot and records nothing. Upgrading from 6.x? See [Upgrading to 7.0](#upgrading-to-70).
79
+
78
80
  ### No custom code or middleware required
79
81
 
80
82
  With older versions of Coverband, projects would report to redis using rack or sidekiq middleware. After Coverband 4.0, this should no longer be required and could cause performance issues. Reporting to redis is now automatically done within a background thread with no custom code needed.
@@ -97,6 +99,46 @@ use Coverband::BackgroundMiddleware
97
99
  run ActionController::Dispatcher.new
98
100
  ```
99
101
 
102
+ ## Upgrading to 7.0
103
+
104
+ 7.0 is a major release with breaking changes. Most apps only need the first two steps. The full list is in the [changelog](https://github.com/danmayer/coverband/blob/main/changes.md).
105
+
106
+ 1. **Add the `redis` gem if you use Redis.** Coverband no longer installs it for you. Redis is still the default store and still needs no other configuration once the gem is in your Gemfile:
107
+
108
+ ```ruby
109
+ gem 'coverband'
110
+ gem 'redis'
111
+ ```
112
+
113
+ Without it, and without a configured `config.store`, Coverband logs a warning at boot and records nothing.
114
+
115
+ 2. **Coverage starts fresh.** The storage format changed, so data recorded by 6.x is ignored rather than migrated. Once you are happy with the new data, delete the old keys with `rake coverband:clear_legacy` (Redis only).
116
+
117
+ 3. **Delete removed settings from `config/coverband.rb`.** These now do nothing. Assigning one logs a warning naming the setting, but the app still boots:
118
+
119
+ | Setting | What to do |
120
+ | --- | --- |
121
+ | `reporter` | Delete it. It has had no effect since Coverband 5.0. |
122
+ | `api_key`, `service_url`, `coverband_timeout`, `service_dev_mode`, `service_test_mode`, `process_type` | Delete them, see the coverband.io step below. |
123
+ | `s3_region`, `s3_bucket`, `s3_access_key_id`, `s3_secret_access_key`, `track_gems`, `gem_details`, `simulate_oneshot_lines_coverage` | Delete them. They were already no-ops. |
124
+
125
+ 4. **coverband.io users: configure a store.** coverband.io is no longer maintained, so reporting to it (`WebServiceStore`) has been removed, along with `config/coverband_service.rb` and the `COVERBAND_API_KEY`, `COVERBAND_URL`, `COVERBAND_ENABLE_DEV_MODE`, `COVERBAND_ENABLE_TEST_MODE`, and `PROCESS_TYPE` environment variables. Move any settings from `config/coverband_service.rb` into `config/coverband.rb` and use Redis (step 1) or an [ActiveSupport::Cache store](#activesupportcache-store).
126
+
127
+ 5. **`MemcachedStore` users: switch to `ActiveSupportCacheStore`.** `MemcachedStore` has been removed. The replacement takes the same cache and adds tracker support:
128
+
129
+ ```ruby
130
+ # before
131
+ config.store = Coverband::Adapters::MemcachedStore.new(cache, memcached_namespace: "myapp")
132
+ # after
133
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new(cache, cache_namespace: "myapp")
134
+ ```
135
+
136
+ 6. **Custom code against Coverband internals.** Only relevant if you wrote a tracker or call adapters directly:
137
+ - In a custom tracker, use `storage` instead of `redis_store`.
138
+ - Instead of `store.raw_store`, use `store.cleanup` (`clear_legacy!`, `clear_orphans!`), which returns `nil` for stores that cannot enumerate keys.
139
+ - Replace calls to the misspelled `Coverband::Utils::Results#runtime_relavent_lines` with `#runtime_relevant_lines`; the old spelling was removed in 7.0.
140
+ - `Coverband::Utils::SourceFile` and `Coverband::Utils::Result` take stored report records (`{"data" => [...]}`) rather than bare line arrays.
141
+
100
142
  ## Coverband Web UI
101
143
 
102
144
  ![image](https://raw.github.com/danmayer/coverband/main/docs/coverband_web_ui.png)
@@ -129,15 +171,20 @@ run ActionController::Dispatcher.new
129
171
 
130
172
  The columns in the web UI are as follows:
131
173
 
132
- - **% covered** - Percentage of total relevant lines covered
133
- - **% runtime** - Percentage of the runtime lines covered where runtime lines are lines that are hit after the application has been eagerly loaded
174
+ - **% covered** - Percentage of relevant lines covered, during either eager loading or runtime
175
+ - **% runtime** - Percentage of the runtime relevant lines that were hit at runtime. Runtime relevant lines are the relevant lines that were *not* hit while the application eager loaded, so this is the share of the code that only real usage can exercise (method bodies, branches, etc.) which real usage actually exercised
134
176
  - **Lines** - Total lines in the file including lines unreachable or uncover-able. An unreachable line would be an empty line with no code, comments, or `end` statements.
135
177
  - **Relevant lines** - Lines that are coverable, i.e. not empty
136
- - **Lines runtime** - Total lines minus uncoverable lines minus the lines that are only hit during eager loading of application
178
+ - **Lines covered** - Relevant lines hit during either eager loading or runtime
179
+ - **Lines runtime** - Relevant lines hit at least once at runtime
137
180
  - **Lines missed** - Relevant lines not covered
138
181
  - **Avg hits/line** - Total of coverage to the file divided by relevant lines.
139
182
 
140
- When viewing an individual file, a line of code such as a class or method definition may appear green because it is eager loaded by the application, but still not be hit at all in runtime by actual users.
183
+ The individual file view also lists the file's **runtime relevant lines**, the denominator of **% runtime**. A file only reports 100% runtime when every one of its runtime relevant lines was hit at runtime. If a file was never eager loaded (the default in development), every relevant line is runtime relevant, so **% runtime** matches **% covered**.
184
+
185
+ **Lines runtime** can include load time lines such as class and method definitions. Those lines are hit at runtime when a process loads the file lazily after Coverband switched to runtime coverage, e.g. a console, runner, or job process that does not eager load. Such lines still count as eager loaded for **% runtime** as long as any process loaded them during eager loading.
186
+
187
+ When viewing an individual file, a line of code such as a class or method definition may appear green because it is eager loaded by the application, but still not be hit at all in runtime by actual users. Each line's badge shows its `load` (eager loading) and `runtime` hit counts separately.
141
188
 
142
189
  ![example of a file with lines not hit at runtime](https://user-images.githubusercontent.com/96786/63541229-aa98a580-c4eb-11e9-8eb8-c004fe1369db.png)
143
190
  #### Coverage first seen and Last activity recorded
@@ -233,7 +280,7 @@ Take Coverband for a spin on the live Heroku deployed [Coverband Demo](https://c
233
280
  If you need to configure Coverband, this can be done by creating a `config/coverband.rb` file relative to your project root.
234
281
 
235
282
  - See [lib/coverband/configuration.rb](https://github.com/danmayer/coverband/blob/main/lib/coverband/configuration.rb) for all options
236
- - By default Coverband will try to store data to Redis \* Redis endpoint is looked for in this order: `ENV['COVERBAND_REDIS_URL']`, `ENV['REDIS_URL']`, or `localhost`
283
+ - By default Coverband will try to store data to Redis (when the `redis` gem is installed) \* Redis endpoint is looked for in this order: `ENV['COVERBAND_REDIS_URL']`, `ENV['REDIS_URL']`, or `localhost`
237
284
 
238
285
  Below is an example config file for a Rails 5 app:
239
286
 
@@ -309,6 +356,110 @@ When enabled, Coverband subscribes to the `render.view_component` notification a
309
356
 
310
357
  ![image](https://raw.github.com/danmayer/coverband/main/docs/coverband_view_tracker.png)
311
358
 
359
+ ### Query Burst Tracking
360
+
361
+ Coverband includes an optional Query Burst Tracker to monitor SQL activity per request and per background job in production. It helps identify potential N+1 query issues and bursty database behavior.
362
+
363
+ Enable it in your Coverband configuration:
364
+
365
+ ```ruby
366
+ Coverband.configure do |config|
367
+ config.track_query_bursts = true
368
+
369
+ # Thresholds used to flag heavy requests/jobs
370
+ config.query_burst_query_count_threshold = 30
371
+ config.query_burst_sql_time_threshold_ms = 100.0
372
+ end
373
+ ```
374
+
375
+ When enabled, Coverband records stats by execution context (for example controller/action and ActiveJob class) and exposes them in the Query Burst Tracker tab in the web UI.
376
+
377
+ #### Threshold Settings
378
+
379
+ - `query_burst_query_count_threshold`:
380
+ - Max SQL query count allowed for a single tracked execution (one request or one job run).
381
+ - If query count is greater than or equal to this value, it is counted as a threshold hit.
382
+ - `query_burst_sql_time_threshold_ms`:
383
+ - Max total SQL time (in milliseconds) allowed for a single tracked execution.
384
+ - If SQL time is greater than or equal to this value, it is counted as a threshold hit.
385
+
386
+ Threshold logic is OR-based: Coverband records a threshold hit if either threshold is exceeded.
387
+
388
+ #### What You See in the UI
389
+
390
+ For each tracked key, the Query Burst Tracker reports:
391
+
392
+ - Requests: Number of tracked executions
393
+ - Total SQL Calls: Sum of all SQL calls across executions
394
+ - Total SQL ms: Sum of SQL time across executions
395
+ - Max SQL Calls: Highest query count seen in one execution
396
+ - Max SQL ms: Highest SQL time seen in one execution
397
+ - Threshold Hits: Number of executions that exceeded either threshold
398
+
399
+ #### How To Tune Thresholds
400
+
401
+ - Start with defaults (`30` queries, `100.0` ms SQL time).
402
+ - Lower thresholds to detect smaller regressions sooner.
403
+ - Raise thresholds if you see too much noise from expected heavy endpoints/jobs.
404
+ - Tune by endpoint/job behavior: latency-sensitive paths often need stricter limits than batch work.
405
+
406
+ Tip: use this tracker in production-like traffic, where query patterns are most realistic.
407
+
408
+ ### Writing a Custom Tracker
409
+
410
+ Trackers can be shipped by an application or a separate gem without changing Coverband. A tracker subclasses `Coverband::Collectors::AbstractTracker`, registers itself once when its file is required, and is initialized at Rails boot when its enablement block returns true.
411
+
412
+ ```ruby
413
+ # lib/my_app/coverband/feature_flag_tracker.rb
414
+ class FeatureFlagTracker < Coverband::Collectors::AbstractTracker
415
+ # required: the path this tracker reports on inside the Coverband web report,
416
+ # and the label for its tab
417
+ REPORT_ROUTE = "feature_flag_tracker"
418
+ TITLE = "Feature Flags"
419
+
420
+ def railtie!
421
+ tracker = self
422
+ ActiveSupport::Notifications.subscribe("feature_flag.my_app") do |_name, _start, _finish, _id, payload|
423
+ tracker.track_key(payload.fetch(:key))
424
+ end
425
+ end
426
+
427
+ private
428
+
429
+ def concrete_target
430
+ MyApp::FeatureFlags.all.map(&:key)
431
+ end
432
+ end
433
+
434
+ Coverband::Configuration.add_tracker_flag(:track_feature_flags, default: false)
435
+ Coverband::Collectors::TrackerRegistry.register(
436
+ :feature_flag_tracker,
437
+ tracker_class: FeatureFlagTracker,
438
+ enabled: ->(config) { config.track_feature_flags }
439
+ )
440
+ ```
441
+
442
+ Require the tracker before enabling it in `config/coverband.rb`:
443
+
444
+ ```ruby
445
+ require "my_app/coverband/feature_flag_tracker"
446
+
447
+ Coverband.configure do |config|
448
+ config.track_feature_flags = true
449
+ end
450
+ ```
451
+
452
+ Every tracker must declare its own `REPORT_ROUTE`. `AbstractTracker`'s default of `"/"` matches every path in the web report, so a tracker that inherits it would answer all of Coverband's own report routes. Registering such a tracker raises `ArgumentError` rather than letting it take over the UI.
453
+
454
+ `tracker_class` may also be a callable that returns a tracker instance when initialization needs to choose an implementation. Registration and enablement are evaluated at boot, not on every request. Duplicate tracker names raise `ArgumentError` so load-order mistakes fail early. Coverband's Query Burst Tracker is a bundled example of the same registration pattern.
455
+
456
+ Trackers registered this way have no dedicated `Coverband.configuration` accessor, so reach them by their registered name:
457
+
458
+ ```ruby
459
+ Coverband.configuration.tracker_for(:feature_flag_tracker)
460
+ Coverband.track_key(:feature_flag_tracker, "flags.new_checkout")
461
+ ```
462
+
312
463
  ### Hiding settings
313
464
 
314
465
  Coverband provides a view of all of its current settings. Sometimes you might want to hide this view,
@@ -317,6 +468,24 @@ You can disable the settings view like so:
317
468
 
318
469
  `config.hide_settings = true`
319
470
 
471
+ ### Dead-code review session
472
+
473
+ The web report includes a client-side "dead-code session" toolbar to help drive code-removal work:
474
+
475
+ - Hide files or whole folders from the report via the ✕ icon that appears when hovering path segments, or hide everything with runtime % > 0 with one toggle.
476
+ - Mark files or folders for deletion via the 🗑 icon (with an optional comment). Marked items disappear from the view and are listed in the "Marked for deletion" modal.
477
+ - Export the marked list as a CSV (`path,comment,marked_at`) to feed a code-removal PR process.
478
+
479
+ Session state lives in your browser's localStorage: "Reset session" clears the hidden set; "Start over" (in the modal) clears the marked list.
480
+
481
+ ### Dark mode
482
+
483
+ The web report follows your operating system's appearance setting through the CSS `prefers-color-scheme` media query. There is nothing to configure and no toggle in the UI: if your desktop is in dark mode, the report is dark, and it switches back when your desktop does. Printing always uses the light theme.
484
+
485
+ The coverage row colors (covered / missed / never / skipped) are chosen for contrast on the dark surface rather than darkened mechanically, so the states stay as easy to tell apart as in the light theme.
486
+
487
+ If you restyle the report, every color in `public/application.css` resolves through a custom property defined on `:root`, and the dark theme is a single `@media (prefers-color-scheme: dark)` block that redefines those properties. Overriding a property is enough to change both themes from one place.
488
+
320
489
  ### Fixing Coverage Only Shows Loading Hits
321
490
 
322
491
  If all your coverage is being counted as loading or eager_loading coverage, and nothing is showing as runtime Coverage the initialization hook failed for some reason. The most likely reason for this issue is manually calling `eager_load!` on some Plugin/Gem. If you or a plugin is altering the Rails initialization process, you can manually flip Coverband to runtime coverage by calling these two lines, in an `after_initialize` block, in `application.rb`.
@@ -382,6 +551,55 @@ Coverband on very high volume sites with many server processes reporting can hav
382
551
 
383
552
  See more discussion [here](https://github.com/danmayer/coverband/issues/384).
384
553
 
554
+ ### ActiveSupport::Cache Store
555
+
556
+ Coverband can store coverage in any `ActiveSupport::Cache::Store`, which covers
557
+ Redis, Memcached, files, and — through [Solid Cache](https://github.com/rails/solid_cache) —
558
+ Postgres, MySQL, and SQLite.
559
+
560
+ ```ruby
561
+ # Rails.cache does not exist yet when config/coverband.rb loads, so pass a block
562
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new { Rails.cache }
563
+
564
+ # or hand it a store directly
565
+ config.store = Coverband::Adapters::ActiveSupportCacheStore.new(
566
+ ActiveSupport::Cache::MemCacheStore.new("localhost:11211")
567
+ )
568
+ ```
569
+
570
+ The block form is the one to use with Solid Cache: its models come from a Rails
571
+ engine that has not loaded when `config/coverband.rb` runs, so building a
572
+ `SolidCache::Store` there raises `uninitialized constant
573
+ SolidCache::Store::Entries::Entry`.
574
+
575
+ Coverband starts from `before_configuration`, which is earlier than Rails
576
+ assigns `Rails.cache`, so the first reporting cycle can run before the block can
577
+ resolve. That cycle's coverage is held and written once the cache appears; it is
578
+ not lost, and it does not raise into the booting app.
579
+
580
+ This adapter supports the view, route, translation, and query burst trackers.
581
+ It replaces `MemcachedStore`, which was removed in 7.0 (see
582
+ [Upgrading to 7.0](#upgrading-to-70)).
583
+
584
+ Things to know before pointing it at a cache:
585
+
586
+ - **A cache may evict what Coverband writes.** Use a store that will not expire
587
+ the data: a dedicated Solid Cache database with `max_age: nil` plus a size
588
+ limit, or a Redis instance with no eviction policy. Coverband detects the loss
589
+ and reports it in the web UI when it can, but it cannot prevent it.
590
+ - **Memcached's 1MB value limit applies to every document**, trackers included.
591
+ A large translation or view tracker can exceed it, in which case the write is
592
+ refused and Coverband logs what could not be stored. Raise the slab limit
593
+ (`-I`) or use Redis/Solid Cache for large apps.
594
+ - **`ActiveSupport::Cache::MemoryStore` is per process**, so it is only useful
595
+ for development and tests.
596
+ - **Solid Cache** writes go through ActiveRecord, so size your connection pool
597
+ for one extra connection per process, and prefer a separate cache database so
598
+ Coverband's writes do not evict your application's own cached data.
599
+ - For very large multi-process deployments `Coverband::Adapters::HashRedisStore`
600
+ is still the best option: it avoids write conflicts entirely rather than
601
+ repairing them.
602
+
385
603
  ### Clear Coverage
386
604
 
387
605
  Now that Coverband uses MD5 hashes there should be no reason to manually clear coverage unless one is testing, changing versions, or possibly debugging Coverband itself.
@@ -393,6 +611,28 @@ This will clear both the coverage and trackers data. It can also be done through
393
611
  To clear only coverage data, run `rake coverband:clear_coverage`.
394
612
  To clear only trackers data, run `rake coverband:clear_tracker`.
395
613
 
614
+ Redis-backed stores also support two maintenance tasks:
615
+
616
+ - `rake coverband:clear_legacy` removes Coverband-owned keys from storage
617
+ formats retired by the 7.0 upgrade.
618
+ - `rake coverband:clear_orphans` removes old generation documents that no
619
+ pointer references, after rechecking that each candidate is not live or too
620
+ young to delete safely.
621
+
622
+ In Coverband 7.0, adapters no longer expose their backend through `raw_store`.
623
+ Code that needs these supported maintenance operations can request the narrow
624
+ capability instead:
625
+
626
+ ```ruby
627
+ cleanup = Coverband.configuration.store.cleanup
628
+ cleanup&.clear_legacy!
629
+ cleanup&.clear_orphans!
630
+ ```
631
+
632
+ `RedisStore` and `HashRedisStore` provide this capability. Adapters that cannot
633
+ enumerate their keys return `nil`; clear the backing cache or rely on its expiry
634
+ policy for those stores.
635
+
396
636
  ### Adding Rake Tasks outside of Rails
397
637
 
398
638
  Rails apps should automatically include the tasks via the Railtie.
@@ -491,6 +731,28 @@ The MCP server provides the following tools:
491
731
  | `get_view_tracker_data` | Get view/template usage data |
492
732
  | `get_route_tracker_data` | Get route usage statistics |
493
733
  | `get_translation_tracker_data` | Get translation key usage data |
734
+ | `get_storage_health` | Get observed coverage and tracker storage protocol state |
735
+
736
+ #### Storage health
737
+
738
+ `Coverband.storage_health` returns normalized diagnostics for coverage and every initialized
739
+ tracker. The normal JSON report also includes this value as its top-level `storage_health`
740
+ field, and MCP exposes the same representation through `get_storage_health`:
741
+
742
+ ```ruby
743
+ Coverband.storage_health
744
+ # => {
745
+ # coverage: {status: "ok", data_loss: nil, unwritten: nil},
746
+ # trackers: {view_tracker: {status: "stalled", data_loss: nil,
747
+ # unwritten: {deltas: 3, since: "2026-08-19T12:01:00Z"}}}
748
+ # }
749
+ ```
750
+
751
+ Statuses are `ok`, `stalled`, `data_loss`, or `unsupported`; `data_loss` takes precedence
752
+ when both loss and unwritten work are present. These diagnostics describe state Coverband's
753
+ storage protocol has already observed. They are not an active backend availability or
754
+ connectivity probe. SimpleCov-compatible JSON generated with `for_merged_report` does not
755
+ include this metadata.
494
756
 
495
757
  #### Running the MCP Server
496
758
 
@@ -614,7 +876,7 @@ Outputs:
614
876
  # Prerequisites
615
877
 
616
878
  - Ruby 3.1+
617
- - Coverband currently requires Redis for production usage
879
+ - A production store: Redis by default (add the `redis` gem), or any `ActiveSupport::Cache` store such as Solid Cache or memcached
618
880
 
619
881
  ### Ruby and Rails Version Support
620
882
 
@@ -662,13 +924,16 @@ If you submit a change please make sure the tests and benchmarks are passing.
662
924
  - **total fail** on front end code, for line for line coverage, because of the precompiled template step basically coverage doesn't work well for `erb`, `slim`, and the like.
663
925
  - related it will try to report something, but the line numbers reported for `ERB` files are often off and aren't considered useful. I recommend filtering out .erb using the `config.ignore` option. The default configuration excludes these files
664
926
  - **NOTE:** We now have file level coverage for view files, but don't support line level detail
665
- - **Coverage does NOT work when used alongside Scout APM Auto Instrumentation**
666
- - In an environment that uses Scout's `AUTO_INSTRUMENT=true` (usually production or staging) it stops reporting any coverage, it will show one or two files that have been loaded at the start but everything else will show up as having 0% coverage
927
+ - **Scout APM Auto Instrumentation** — fixed in Ruby 3.4.10 / 4.0.4, broken on earlier Rubies
928
+ - **Symptom:** in an environment that uses Scout's auto instruments (usually production or staging) Coverband stops reporting coverage. You will see one or two files loaded at the start, and everything else shows up as 0% / never loaded. This is dangerous: live code, particularly your controllers, gets reported as dead code.
929
+ - **Cause:** this is a Ruby bug, not a bug in Coverband or Scout. Scout's auto instrumentation installs a `RubyVM::InstructionSequence.load_iseq` hook and loads your files through `RubyVM::InstructionSequence.compile` / `.compile_file`. Until recently those methods did not register a file with the `Coverage` module the way a normal `require` does, so the file was never measured and never even appeared in `Coverage.result`. Note that this affected *every* file loaded while the hook was installed, not just the controllers Scout rewrites, which is why coverage appears to vanish entirely.
930
+ - **Fix:** upgrade to **Ruby >= 3.4.10 or >= 4.0.4**, which include the fix for [Ruby Bug #22018](https://bugs.ruby-lang.org/issues/22018). Coverband and Scout auto instruments then work together with no configuration changes and no minimum version of either gem. Both `lines` and `oneshot_lines` collection modes are unaffected.
931
+ - **Workaround for Ruby <= 3.3:** the fix was not backported to Ruby 3.1, 3.2, or 3.3, so on those versions you must disable auto instruments to collect coverage — set `auto_instruments: false` in `scout_apm.yml` or `SCOUT_AUTO_INSTRUMENTS=false` in the environment.
667
932
  - Bug tracked here: https://github.com/scoutapp/scout_apm_ruby/issues/343
668
933
  - **Coverband, [Elastic APM](https://github.com/elastic/apm-agent-ruby) and resque**
669
934
  - In an environment that uses the Elastic APM ruby agent, resque jobs will fail with `Transactions may not be nested. Already inside #<ElasticAPM::Transaction>` if the `elastic-apm` gem is loaded _before_ the `coverband` gem
670
935
  - Put `coverage` ahead of `elastic-apm` in your Gemfile
671
- - **Bootsnap**, The methods used by [bootsnap do not support having coverage enabled](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/compile_cache/iseq.rb#L87), so you can either have Coverband or bootsnap, but not both.
936
+ - **Bootsnap**, you can run bootsnap alongside Coverband, but you lose part of the boot time speedup. [Bootsnap detects that coverage is enabled](https://github.com/Shopify/bootsnap/blob/main/lib/bootsnap/compile_cache/iseq.rb) and disables its `ISeq` compile cache, because a cached instruction sequence would not carry the `Coverage` instrumentation. Bootsnap's other caches (load path, YAML, JSON) are unaffected and keep working.
672
937
 
673
938
  ### Debugging Redis Store
674
939
 
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RuboCop::RakeTask.new
8
8
 
9
9
  task default: %i[test]
10
10
 
11
- task "test:all": %i[test forked_tests benchmarks:memory benchmarks]
11
+ task "test:all": %i[test test:javascript forked_tests benchmarks:memory benchmarks]
12
12
 
13
13
  task :test
14
14
  require "rake/testtask"
@@ -21,6 +21,30 @@ Rake::TestTask.new(:test) do |test|
21
21
  test.verbose = true
22
22
  end
23
23
 
24
+ ###
25
+ # Solid Cache brings a Rails engine with it, which changes how the rest of the
26
+ # suite behaves in the same process. It gets its own task and its own process.
27
+ ###
28
+ desc "run the Solid Cache backed adapter tests (SQLite, needs solid_cache)"
29
+ task :"test:solid_cache" do
30
+ # BUNDLE_WITH opts the optional group onto the load path for this run only.
31
+ # It has to already be installed: `bundle install --with solid_cache`.
32
+ sh({"COVERBAND_SOLID_CACHE" => "true", "BUNDLE_WITH" => "solid_cache"},
33
+ "bundle exec ruby -Ilib -Itest test/coverband/adapters/solid_cache_store_test.rb") do |ok, _res|
34
+ unless ok
35
+ abort "solid_cache tests failed. If the gems are missing, run: bundle install --with solid_cache"
36
+ end
37
+ end
38
+ end
39
+
40
+ desc "run the web report's javascript unit tests (node --test)"
41
+ task :"test:javascript" do
42
+ # Pass expanded file paths rather than the bare directory: node's
43
+ # directory-argument handling for --test is inconsistent across versions
44
+ # (Node 22 fails to find test/javascript/ as a directory in some CI images).
45
+ sh "node --test #{FileList["test/javascript/*.test.mjs"].join(" ")}"
46
+ end
47
+
24
48
  Rake::TestTask.new(:forked_tests) do |test|
25
49
  if RUBY_PLATFORM == "java"
26
50
  puts "forked tests not supported on JRuby"
@@ -43,8 +67,3 @@ desc "start webserver"
43
67
  task :server do
44
68
  exec "rackup -I lib"
45
69
  end
46
-
47
- desc "publish gem with 2 factor auth, reminder how"
48
- task :publish_gem do
49
- exec "gem push pkg/coverband-4.2.3.XXX.gem"
50
- end