coverband 6.2.2 → 7.0.0.rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/diagram.yml +2 -2
  3. data/.github/workflows/main.yml +64 -7
  4. data/Gemfile +16 -0
  5. data/Gemfile.rails7.0 +12 -0
  6. data/Gemfile.rails7.1 +6 -1
  7. data/Gemfile.rails7.2 +5 -0
  8. data/Gemfile.rails8.0 +15 -0
  9. data/README.md +218 -12
  10. data/Rakefile +16 -5
  11. data/changes.md +60 -0
  12. data/coverband.gemspec +15 -13
  13. data/lib/coverband/adapters/active_support_cache_store.rb +54 -0
  14. data/lib/coverband/adapters/base.rb +42 -10
  15. data/lib/coverband/adapters/file_store.rb +2 -2
  16. data/lib/coverband/adapters/hash_redis_store.rb +22 -3
  17. data/lib/coverband/adapters/null_store.rb +0 -4
  18. data/lib/coverband/adapters/redis_cleanup.rb +101 -0
  19. data/lib/coverband/adapters/redis_store.rb +32 -65
  20. data/lib/coverband/adapters/session_coverage.rb +172 -0
  21. data/lib/coverband/adapters/stdout_store.rb +0 -4
  22. data/lib/coverband/adapters/tracker_storage/base.rb +79 -0
  23. data/lib/coverband/adapters/tracker_storage/document_repository.rb +49 -0
  24. data/lib/coverband/adapters/tracker_storage/factory.rb +57 -0
  25. data/lib/coverband/adapters/tracker_storage/redis_hash_repository.rb +127 -0
  26. data/lib/coverband/collectors/abstract_tracker.rb +135 -51
  27. data/lib/coverband/collectors/query_burst_tracker.rb +72 -19
  28. data/lib/coverband/collectors/route_tracker.rb +6 -0
  29. data/lib/coverband/collectors/tracker_registry.rb +63 -0
  30. data/lib/coverband/collectors/translation_tracker.rb +6 -0
  31. data/lib/coverband/collectors/view_tracker.rb +11 -5
  32. data/lib/coverband/configuration.rb +95 -131
  33. data/lib/coverband/mcp/http_handler.rb +1 -1
  34. data/lib/coverband/mcp/server.rb +4 -1
  35. data/lib/coverband/mcp/tools/get_storage_health.rb +28 -0
  36. data/lib/coverband/reporters/base.rb +0 -2
  37. data/lib/coverband/reporters/html_report.rb +2 -3
  38. data/lib/coverband/reporters/json_report.rb +2 -2
  39. data/lib/coverband/reporters/web.rb +72 -15
  40. data/lib/coverband/storage/document.rb +173 -0
  41. data/lib/coverband/storage/generation.rb +165 -0
  42. data/lib/coverband/storage/generation_change.rb +53 -0
  43. data/lib/coverband/storage/generation_coordinator.rb +145 -0
  44. data/lib/coverband/storage/io_guard.rb +29 -0
  45. data/lib/coverband/storage/read_fallback.rb +30 -0
  46. data/lib/coverband/storage/redis_target.rb +74 -0
  47. data/lib/coverband/storage/session.rb +527 -0
  48. data/lib/coverband/storage/target.rb +117 -0
  49. data/lib/coverband/storage/writer.rb +194 -0
  50. data/lib/coverband/storage_health.rb +90 -0
  51. data/lib/coverband/utils/configuration_template.rb +14 -1
  52. data/lib/coverband/utils/railtie.rb +2 -2
  53. data/lib/coverband/utils/result.rb +5 -5
  54. data/lib/coverband/utils/results.rb +32 -16
  55. data/lib/coverband/utils/source_file.rb +17 -27
  56. data/lib/coverband/utils/tasks.rb +71 -6
  57. data/lib/coverband/version.rb +1 -1
  58. data/lib/coverband.rb +98 -20
  59. data/public/application.css +540 -146
  60. data/test/benchmarks/benchmark.rake +13 -0
  61. data/test/coverband/adapters/active_support_cache_store_test.rb +613 -0
  62. data/test/coverband/adapters/capabilities_test.rb +80 -0
  63. data/test/coverband/adapters/redis_store_test.rb +63 -2
  64. data/test/coverband/adapters/solid_cache_store_test.rb +181 -0
  65. data/test/coverband/adapters/tracker_storage_test.rb +170 -0
  66. data/test/coverband/collectors/query_burst_tracker_test.rb +155 -10
  67. data/test/coverband/collectors/route_tracker_test.rb +2 -13
  68. data/test/coverband/collectors/tracker_registry_test.rb +218 -0
  69. data/test/coverband/collectors/translation_tracker_test.rb +76 -16
  70. data/test/coverband/collectors/view_tracker_test.rb +49 -12
  71. data/test/coverband/configuration_test.rb +36 -38
  72. data/test/coverband/mcp/server_test.rb +2 -1
  73. data/test/coverband/mcp/tools/get_storage_health_test.rb +41 -0
  74. data/test/coverband/reporters/html_test.rb +26 -0
  75. data/test/coverband/reporters/json_test.rb +53 -0
  76. data/test/coverband/reporters/web_test.rb +96 -0
  77. data/test/coverband/storage/generation_coordinator_test.rb +191 -0
  78. data/test/coverband/storage/io_guard_test.rb +127 -0
  79. data/test/coverband/storage/protocol_conformance_test.rb +725 -0
  80. data/test/coverband/storage_health_test.rb +99 -0
  81. data/test/coverband/track_key_test.rb +8 -0
  82. data/test/coverband/utils/cleanup_tasks_test.rb +188 -0
  83. data/test/coverband/utils/file_list_test.rb +2 -2
  84. data/test/coverband/utils/result_test.rb +4 -4
  85. data/test/coverband/utils/results_test.rb +100 -10
  86. data/test/coverband/utils/source_file_test.rb +40 -6
  87. data/test/forked/rails_cache_full_stack_test.rb +64 -0
  88. data/test/forked/rails_full_stack_test.rb +7 -0
  89. data/test/integration/full_stack_deferred_eager_test.rb +0 -1
  90. data/test/integration/full_stack_send_deferred_eager_test.rb +0 -1
  91. data/test/integration/full_stack_test.rb +0 -1
  92. data/test/integration/mcp_integration_test.rb +4 -2
  93. data/test/integration/standalone_json_report_test.rb +46 -0
  94. data/test/integration/without_redis_test.rb +76 -0
  95. data/test/rails7_dummy/config/application.rb +1 -0
  96. data/test/rails7_dummy/config/coverband_missing_redis.rb +0 -2
  97. data/test/rails7_dummy/config/coverband_rails_cache.rb +16 -0
  98. data/test/rails8_dummy/config/application.rb +1 -0
  99. data/test/rails8_dummy/config/coverband_rails_cache.rb +3 -0
  100. data/test/rails_test_helper.rb +2 -1
  101. data/test/test_helper.rb +5 -17
  102. data/views/file_list.erb +2 -2
  103. data/views/source_file.erb +4 -3
  104. metadata +48 -40
  105. data/lib/coverband/adapters/memcached_store.rb +0 -85
  106. data/lib/coverband/adapters/web_service_store.rb +0 -152
  107. data/lib/coverband/collectors/view_tracker_service.rb +0 -59
  108. data/test/coverband/adapters/memecached_store_test.rb +0 -26
  109. data/test/coverband/adapters/web_service_store_test.rb +0 -56
@@ -32,6 +32,16 @@ module Coverband
32
32
  @keys_to_record = Set.new
33
33
  end
34
34
 
35
+ ###
36
+ # Whether two recordings of the same key combine to the same result.
37
+ # Presence trackers take the later timestamp, so they do; one accumulating
38
+ # counters has to say so, since re-applying a sum double counts and the
39
+ # storage layer picks its layout from this.
40
+ ###
41
+ def self.idempotent_merge?
42
+ true
43
+ end
44
+
35
45
  def logged_keys
36
46
  @logged_keys.to_a
37
47
  end
@@ -53,9 +63,12 @@ module Coverband
53
63
  end
54
64
 
55
65
  def used_keys
56
- return {} unless redis_store
66
+ return {} unless storage
57
67
 
58
- redis_store.hgetall(tracker_key)
68
+ storage.entries
69
+ rescue => e
70
+ logger&.error "Coverband: #{self.class.name} failed to read, error #{e.class.name} info #{e.message}"
71
+ {}
59
72
  end
60
73
 
61
74
  def all_keys
@@ -76,43 +89,63 @@ module Coverband
76
89
  end
77
90
 
78
91
  def tracking_since
79
- return "N/A" unless redis_store
92
+ return "N/A" unless storage
80
93
 
81
- if (tracking_time = redis_store.get(tracker_time_key))
82
- Time.at(tracking_time.to_i).iso8601
83
- else
84
- "N/A"
85
- end
94
+ (tracking_time = storage.tracking_since) ? tracking_time.iso8601 : "N/A"
86
95
  end
87
96
 
97
+ # local state is dropped by the generation change callback, which only
98
+ # fires once the new pointer is durable; clearing here first would throw
99
+ # away unsaved work on a reset that never landed
88
100
  def reset_recordings
89
- return unless redis_store
101
+ return unless storage
90
102
 
91
- redis_store.del(tracker_key)
92
- redis_store.del(tracker_time_key)
103
+ storage.reset
93
104
  end
94
105
 
95
106
  def clear_key!(key)
96
107
  return unless key
97
- return unless redis_store
108
+ return unless storage
98
109
 
99
- puts "#{tracker_key} key #{key}"
100
- redis_store.hdel(tracker_key, key)
110
+ storage.delete_entry(key)
101
111
  @logged_keys.delete(key)
112
+ @keys_to_record.delete(key)
102
113
  end
103
114
 
115
+ # loss the storage layer noticed and repaired as best it could, surfaced so
116
+ # the report can say the numbers are partial rather than imply they are not
117
+ def data_loss
118
+ storage&.data_loss
119
+ end
120
+
121
+ # so a tab showing nothing can say why, rather than reading as an app that
122
+ # used nothing
123
+ def unwritten
124
+ storage&.unwritten
125
+ end
126
+
127
+ # exposed so a reporting cycle can batch every pointer read it will make
128
+ def pointer_session
129
+ storage&.pointer_session
130
+ end
131
+
132
+ ###
133
+ # The work is neither stored nor held for us. Keeping our keys against
134
+ # these is what makes an outage of any length lossless, since this set is
135
+ # unbounded where the storage queue is capped. Anything else means storage
136
+ # took it, and a second copy would enqueue the same work twice.
137
+ ###
138
+ UNSTORED_STATES = %i[failed unavailable].freeze
139
+
104
140
  def save_report
105
- return unless redis_store
141
+ return unless storage
106
142
 
107
- redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
108
- @one_time_timestamp = true
109
- reported_time = Time.now.to_i
110
- if @keys_to_record.any?
111
- redis_store.hset(tracker_key, @keys_to_record.each_with_object({}) { |key, h| h[key.to_s] = reported_time })
112
- end
113
- @keys_to_record.clear
143
+ forget_deleted_keys
144
+ result = storage.record(delta_to_record)
145
+ @keys_to_record.clear unless UNSTORED_STATES.include?(result)
146
+ recover_dropped_keys
114
147
  rescue => e
115
- # we don't want to raise errors if Coverband can't reach redis.
148
+ # we don't want to raise errors if Coverband can't reach its store.
116
149
  # This is a nice to have not a bring the system down
117
150
  logger&.error "Coverband: #{self.class.name} failed to store, error #{e.class.name} info #{e.message}"
118
151
  end
@@ -132,6 +165,59 @@ module Coverband
132
165
 
133
166
  protected
134
167
 
168
+ def delta_to_record
169
+ return {} if @keys_to_record.empty?
170
+
171
+ reported_time = Time.now.to_i
172
+ @keys_to_record.each_with_object({}) { |key, hash| hash[key.to_s] = reported_time }
173
+ end
174
+
175
+ # a key another process cleared has to leave the dedupe set, or a later
176
+ # genuine use of it would never be enqueued again
177
+ def forget_deleted_keys
178
+ storage.newly_tombstoned.each do |key|
179
+ @logged_keys.delete_if { |logged| logged.to_s == key }
180
+ @keys_to_record.delete_if { |pending| pending.to_s == key }
181
+ end
182
+ end
183
+
184
+ ###
185
+ # A reset means the operator wants everything gone. An eviction means the
186
+ # backend lost data this process can partly reconstruct, so the keys it
187
+ # already knows are queued to be reported once more rather than forgotten.
188
+ ###
189
+ def drop_local_state!(change)
190
+ case change.cause
191
+ when Storage::GenerationChange::POINTER_EVICTION,
192
+ Storage::GenerationChange::DOCUMENT_EVICTION
193
+ @logged_keys.each { |key| @keys_to_record << key if track_key?(key) }
194
+ when Storage::GenerationChange::OPERATOR_RESET
195
+ @logged_keys.clear
196
+ @keys_to_record.clear
197
+ end
198
+ end
199
+
200
+ ###
201
+ # Work dropped at the storage caps is gone and the dedupe set has moved on,
202
+ # so those keys would never be reported again: a used view reading as
203
+ # unused, the dangerous direction for anything driving deletion. What is
204
+ # known is queued again, the way an eviction already is.
205
+ #
206
+ # Only sound where the merge is idempotent -- re-supplying a summed counter
207
+ # is the double count :retained exists to prevent. Keyed on the loss
208
+ # timestamp, so an outage re-supplies once per drop, not once per cycle.
209
+ ###
210
+ def recover_dropped_keys
211
+ return unless self.class.idempotent_merge?
212
+
213
+ loss = storage.data_loss
214
+ return unless loss && loss.kind == :pending_dropped
215
+ return if @recovered_loss_at == loss.at
216
+
217
+ @recovered_loss_at = loss.at
218
+ @logged_keys.each { |key| @keys_to_record << key if track_key?(key) }
219
+ end
220
+
135
221
  def newly_seen_key?(key)
136
222
  !@logged_keys.include?(key)
137
223
  end
@@ -141,44 +227,42 @@ module Coverband
141
227
  @ignore_patterns.none? { |pattern| key.match?(pattern) }
142
228
  end
143
229
 
144
- private
145
-
146
- def concrete_target
147
- raise "subclass must implement"
148
- end
230
+ ###
231
+ # Presence merging keeps the later of the two timestamps, so applying the
232
+ # same delta twice changes nothing.
233
+ ###
234
+ def merger
235
+ @merger ||= lambda do |doc, delta|
236
+ delta.payload.each do |key, value|
237
+ next if doc.tombstoned?(key, delta.tombstone_epoch)
149
238
 
150
- def redis_store
151
- @redis_store ||= begin
152
- store.raw_store
153
- rescue NotImplementedError
154
- nil
239
+ existing = doc.payload[key.to_s]
240
+ doc.payload[key.to_s] = value.to_s if existing.nil? || existing.to_i < value.to_i
241
+ end
155
242
  end
156
243
  end
157
244
 
158
- def tracker_time_key_exists?
159
- return false unless redis_store
160
-
161
- if defined?(redis_store.exists?)
162
- redis_store.exists?(tracker_time_key)
163
- else
164
- redis_store.exists(tracker_time_key)
165
- end
245
+ def storage
246
+ return @storage if defined?(@storage) && @storage
247
+
248
+ factory = store.tracker_storage
249
+ @storage = factory&.for(
250
+ class_key,
251
+ merger: merger,
252
+ idempotent: self.class.idempotent_merge?,
253
+ logger: logger,
254
+ on_generation_change: ->(change) { drop_local_state!(change) }
255
+ )
166
256
  end
167
257
 
168
- def tracker_key
169
- "#{class_key}_tracker"
170
- end
258
+ private
171
259
 
172
- def tracker_time_key
173
- "#{class_key}_tracker_time"
260
+ def concrete_target
261
+ raise "subclass must implement"
174
262
  end
175
263
 
176
264
  def class_key
177
- @class_key ||= if Coverband.configuration.redis_namespace
178
- "#{Coverband.configuration.redis_namespace}_#{self.class.name.split("::").last}"
179
- else
180
- self.class.name.split("::").last
181
- end
265
+ @class_key ||= self.class.name.split("::").last
182
266
  end
183
267
  end
184
268
  end
@@ -53,20 +53,18 @@ module Coverband
53
53
  end
54
54
 
55
55
  def used_keys
56
- return {} unless redis_store
56
+ return {} unless storage
57
57
 
58
- stats_hash = redis_store.hgetall(tracker_key)
59
- stats_hash.each_with_object({}) do |(key, stats_json), used|
58
+ storage.entries.each_with_object({}) do |(key, stats_json), used|
60
59
  stats = parse_stats(stats_json)
61
60
  used[key] = stats["last_seen"].to_i
62
61
  end
63
62
  end
64
63
 
65
64
  def used_key_stats
66
- return {} unless redis_store
65
+ return {} unless storage
67
66
 
68
- stats_hash = redis_store.hgetall(tracker_key)
69
- stats_hash
67
+ storage.entries
70
68
  .transform_values { |stats_json| parse_stats(stats_json) }
71
69
  .sort_by { |key, stats| [-stats["threshold_hits"].to_i, -stats["total_sql_time_ms"].to_f, key] }
72
70
  .to_h
@@ -91,23 +89,65 @@ module Coverband
91
89
  }.to_json
92
90
  end
93
91
 
92
+ ###
93
+ # These stats are cumulative, so a lost update can never be noticed by
94
+ # reading the total back and re-applying one would double count. The
95
+ # storage layer's applied sequences are what make the retry safe, which is
96
+ # also why this tracker can't use the per field Redis hash layout.
97
+ ###
94
98
  def save_report
95
- return unless redis_store
96
- return if @pending_stats.empty?
99
+ return unless storage
100
+ # storage may be holding work from a cycle that could not be written,
101
+ # and a quiet cycle is the only chance it gets to flush it
102
+ return if @pending_stats.empty? && storage.pending_size.zero?
103
+
104
+ delta = @pending_stats.each_with_object({}) { |(key, stats), h| h[key] = stats.to_json }
105
+ result = storage.record(delta)
106
+ @pending_stats.clear unless UNSTORED_STATES.include?(result)
107
+ rescue => e
108
+ logger&.error "Coverband: #{self.class.name} failed to store, error #{e.class.name} info #{e.message}"
109
+ end
97
110
 
98
- redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
99
- @one_time_timestamp = true
111
+ ###
112
+ # Counters sum and maxima take the larger value, so this merge is not
113
+ # idempotent: applying the same delta twice inflates the totals.
114
+ ###
115
+ def self.idempotent_merge?
116
+ false
117
+ end
100
118
 
101
- existing_stats = redis_store.hgetall(tracker_key)
102
- merged = @pending_stats.each_with_object({}) do |(key, pending), h|
103
- existing = parse_stats(existing_stats[key])
104
- h[key] = merge_stats(existing, pending).to_json
105
- end
119
+ ###
120
+ # Unsaved aggregates live here rather than in the key sets the parent
121
+ # clears, so the lifecycle hooks have to reach them too. Otherwise counters
122
+ # accumulated before a reset get written into the new generation, and a
123
+ # cleared key comes back with its old totals.
124
+ ###
125
+ protected
106
126
 
107
- redis_store.hset(tracker_key, merged) if merged.any?
108
- @pending_stats.clear
109
- rescue => e
110
- logger&.error "Coverband: #{self.class.name} failed to store, error #{e.class.name} info #{e.message}"
127
+ def drop_local_state!(change)
128
+ super
129
+ # counters are not reconstructible from local state the way presence
130
+ # keys are, so an eviction loses them either way
131
+ @pending_stats&.clear if change.cause == Storage::GenerationChange::OPERATOR_RESET
132
+ end
133
+
134
+ public
135
+
136
+ def clear_key!(key)
137
+ @pending_stats&.delete(key.to_s) if key
138
+ super
139
+ end
140
+
141
+ def merger
142
+ @merger ||= lambda do |doc, delta|
143
+ delta.payload.each do |key, stats_json|
144
+ next if doc.tombstoned?(key, delta.tombstone_epoch)
145
+
146
+ existing = parse_stats(doc.payload[key.to_s])
147
+ incoming = parse_stats(stats_json)
148
+ doc.payload[key.to_s] = merge_stats(existing, incoming).to_json
149
+ end
150
+ end
111
151
  end
112
152
 
113
153
  private
@@ -222,8 +262,15 @@ module Coverband
222
262
  Thread.current[CONTEXT_STACK_KEY] ||= []
223
263
  end
224
264
 
265
+ ###
266
+ # With a database backed cache, Coverband's own storage I/O is SQL. Left
267
+ # unfiltered it lands on whatever controller action or job triggered the
268
+ # report, inflating its query count and potentially tripping the very
269
+ # thresholds this tracker exists to detect.
270
+ ###
225
271
  def ignore_sql_event?(payload)
226
272
  return true if payload[:cached]
273
+ return true if Coverband::Storage::IOGuard.active?
227
274
 
228
275
  name = payload[:name].to_s
229
276
  IGNORED_SQL_NAMES.include?(name)
@@ -266,3 +313,9 @@ module Coverband
266
313
  end
267
314
  end
268
315
  end
316
+
317
+ Coverband::Collectors::TrackerRegistry.register(
318
+ :query_burst_tracker,
319
+ tracker_class: Coverband::Collectors::QueryBurstTracker,
320
+ enabled: ->(config) { config.track_query_bursts }
321
+ )
@@ -99,3 +99,9 @@ module Coverband
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ Coverband::Collectors::TrackerRegistry.register(
104
+ :route_tracker,
105
+ tracker_class: Coverband::Collectors::RouteTracker,
106
+ enabled: ->(config) { config.track_routes }
107
+ )
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Collectors
5
+ ###
6
+ # Registry of the trackers Coverband initializes at Rails boot.
7
+ #
8
+ # Bundled trackers register themselves as their file is required. Apps and
9
+ # gems register their own trackers the same way, which is all Coverband
10
+ # needs to build them, hook them up, and report on them.
11
+ ###
12
+ class TrackerRegistry
13
+ Entry = Struct.new(:name, :tracker_class, :enabled_proc, keyword_init: true)
14
+
15
+ def self.registry
16
+ @registry ||= {}
17
+ end
18
+
19
+ def self.register(name, tracker_class:, enabled:)
20
+ name = name.to_sym
21
+ raise ArgumentError, "Tracker already registered: #{name}" if registry.key?(name)
22
+
23
+ # a callable can only be checked once it has built its tracker at boot
24
+ validate_report_route!(name, tracker_class) unless tracker_class.respond_to?(:call)
25
+
26
+ registry[name] = Entry.new(
27
+ name: name,
28
+ tracker_class: tracker_class,
29
+ enabled_proc: enabled
30
+ )
31
+ end
32
+
33
+ ###
34
+ # A tracker that inherits AbstractTracker's default REPORT_ROUTE matches
35
+ # every path in the web reporter, which hides the rest of the report.
36
+ # Fail loudly rather than let a tracker take over the whole UI.
37
+ ###
38
+ def self.validate_report_route!(name, tracker_class)
39
+ return unless tracker_class.is_a?(Class)
40
+ return unless tracker_class < Coverband::Collectors::AbstractTracker
41
+
42
+ default_route = Coverband::Collectors::AbstractTracker::REPORT_ROUTE
43
+ return unless default_route == tracker_class::REPORT_ROUTE
44
+
45
+ raise ArgumentError, "Tracker #{name} must define its own REPORT_ROUTE, " \
46
+ "AbstractTracker's default #{default_route.inspect} matches every web report path"
47
+ end
48
+
49
+ def self.each(&block)
50
+ registry.each_value(&block)
51
+ end
52
+
53
+ def self.names
54
+ registry.keys
55
+ end
56
+
57
+ # intended for tests, resetting the registry of a booted app is not supported
58
+ def self.reset!
59
+ @registry = {}
60
+ end
61
+ end
62
+ end
63
+ end
@@ -58,3 +58,9 @@ module Coverband
58
58
  end
59
59
  end
60
60
  end
61
+
62
+ Coverband::Collectors::TrackerRegistry.register(
63
+ :translations_tracker,
64
+ tracker_class: Coverband::Collectors::TranslationTracker,
65
+ enabled: ->(config) { config.track_translations }
66
+ )
@@ -68,11 +68,10 @@ module Coverband
68
68
  end
69
69
 
70
70
  def used_keys
71
- return {} unless redis_store
71
+ return {} unless storage
72
72
 
73
- views = redis_store.hgetall(tracker_key)
74
73
  normalized_views = {}
75
- views.each_pair do |view, time|
74
+ storage.entries.each_pair do |view, time|
76
75
  normalized_view = normalize_path(view)
77
76
  normalized_views[normalized_view] = time
78
77
  end
@@ -102,11 +101,12 @@ module Coverband
102
101
 
103
102
  def clear_key!(filename)
104
103
  return unless filename
105
- return unless redis_store
104
+ return unless storage
106
105
 
107
106
  filename = "#{@project_directory}/#{filename}"
108
- redis_store.hdel(tracker_key, filename)
107
+ storage.delete_entry(filename)
109
108
  @logged_keys.delete(filename)
109
+ @keys_to_record.delete(filename)
110
110
  end
111
111
 
112
112
  private
@@ -144,3 +144,9 @@ module Coverband
144
144
  end
145
145
  end
146
146
  end
147
+
148
+ Coverband::Collectors::TrackerRegistry.register(
149
+ :view_tracker,
150
+ tracker_class: Coverband::Collectors::ViewTracker,
151
+ enabled: ->(config) { config.track_views }
152
+ )