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.
- checksums.yaml +4 -4
- data/.github/workflows/diagram.yml +2 -2
- data/.github/workflows/main.yml +64 -7
- data/Gemfile +16 -0
- data/Gemfile.rails7.0 +12 -0
- data/Gemfile.rails7.1 +6 -1
- data/Gemfile.rails7.2 +5 -0
- data/Gemfile.rails8.0 +15 -0
- data/README.md +218 -12
- data/Rakefile +16 -5
- data/changes.md +60 -0
- data/coverband.gemspec +15 -13
- data/lib/coverband/adapters/active_support_cache_store.rb +54 -0
- data/lib/coverband/adapters/base.rb +42 -10
- data/lib/coverband/adapters/file_store.rb +2 -2
- data/lib/coverband/adapters/hash_redis_store.rb +22 -3
- data/lib/coverband/adapters/null_store.rb +0 -4
- data/lib/coverband/adapters/redis_cleanup.rb +101 -0
- data/lib/coverband/adapters/redis_store.rb +32 -65
- data/lib/coverband/adapters/session_coverage.rb +172 -0
- data/lib/coverband/adapters/stdout_store.rb +0 -4
- data/lib/coverband/adapters/tracker_storage/base.rb +79 -0
- data/lib/coverband/adapters/tracker_storage/document_repository.rb +49 -0
- data/lib/coverband/adapters/tracker_storage/factory.rb +57 -0
- data/lib/coverband/adapters/tracker_storage/redis_hash_repository.rb +127 -0
- data/lib/coverband/collectors/abstract_tracker.rb +135 -51
- data/lib/coverband/collectors/query_burst_tracker.rb +72 -19
- data/lib/coverband/collectors/route_tracker.rb +6 -0
- data/lib/coverband/collectors/tracker_registry.rb +63 -0
- data/lib/coverband/collectors/translation_tracker.rb +6 -0
- data/lib/coverband/collectors/view_tracker.rb +11 -5
- data/lib/coverband/configuration.rb +95 -131
- data/lib/coverband/mcp/http_handler.rb +1 -1
- data/lib/coverband/mcp/server.rb +4 -1
- data/lib/coverband/mcp/tools/get_storage_health.rb +28 -0
- data/lib/coverband/reporters/base.rb +0 -2
- data/lib/coverband/reporters/html_report.rb +2 -3
- data/lib/coverband/reporters/json_report.rb +2 -2
- data/lib/coverband/reporters/web.rb +72 -15
- data/lib/coverband/storage/document.rb +173 -0
- data/lib/coverband/storage/generation.rb +165 -0
- data/lib/coverband/storage/generation_change.rb +53 -0
- data/lib/coverband/storage/generation_coordinator.rb +145 -0
- data/lib/coverband/storage/io_guard.rb +29 -0
- data/lib/coverband/storage/read_fallback.rb +30 -0
- data/lib/coverband/storage/redis_target.rb +74 -0
- data/lib/coverband/storage/session.rb +527 -0
- data/lib/coverband/storage/target.rb +117 -0
- data/lib/coverband/storage/writer.rb +194 -0
- data/lib/coverband/storage_health.rb +90 -0
- data/lib/coverband/utils/configuration_template.rb +14 -1
- data/lib/coverband/utils/railtie.rb +2 -2
- data/lib/coverband/utils/result.rb +5 -5
- data/lib/coverband/utils/results.rb +32 -16
- data/lib/coverband/utils/source_file.rb +17 -27
- data/lib/coverband/utils/tasks.rb +71 -6
- data/lib/coverband/version.rb +1 -1
- data/lib/coverband.rb +98 -20
- data/public/application.css +540 -146
- data/test/benchmarks/benchmark.rake +13 -0
- data/test/coverband/adapters/active_support_cache_store_test.rb +613 -0
- data/test/coverband/adapters/capabilities_test.rb +80 -0
- data/test/coverband/adapters/redis_store_test.rb +63 -2
- data/test/coverband/adapters/solid_cache_store_test.rb +181 -0
- data/test/coverband/adapters/tracker_storage_test.rb +170 -0
- data/test/coverband/collectors/query_burst_tracker_test.rb +155 -10
- data/test/coverband/collectors/route_tracker_test.rb +2 -13
- data/test/coverband/collectors/tracker_registry_test.rb +218 -0
- data/test/coverband/collectors/translation_tracker_test.rb +76 -16
- data/test/coverband/collectors/view_tracker_test.rb +49 -12
- data/test/coverband/configuration_test.rb +36 -38
- data/test/coverband/mcp/server_test.rb +2 -1
- data/test/coverband/mcp/tools/get_storage_health_test.rb +41 -0
- data/test/coverband/reporters/html_test.rb +26 -0
- data/test/coverband/reporters/json_test.rb +53 -0
- data/test/coverband/reporters/web_test.rb +96 -0
- data/test/coverband/storage/generation_coordinator_test.rb +191 -0
- data/test/coverband/storage/io_guard_test.rb +127 -0
- data/test/coverband/storage/protocol_conformance_test.rb +725 -0
- data/test/coverband/storage_health_test.rb +99 -0
- data/test/coverband/track_key_test.rb +8 -0
- data/test/coverband/utils/cleanup_tasks_test.rb +188 -0
- data/test/coverband/utils/file_list_test.rb +2 -2
- data/test/coverband/utils/result_test.rb +4 -4
- data/test/coverband/utils/results_test.rb +100 -10
- data/test/coverband/utils/source_file_test.rb +40 -6
- data/test/forked/rails_cache_full_stack_test.rb +64 -0
- data/test/forked/rails_full_stack_test.rb +7 -0
- data/test/integration/full_stack_deferred_eager_test.rb +0 -1
- data/test/integration/full_stack_send_deferred_eager_test.rb +0 -1
- data/test/integration/full_stack_test.rb +0 -1
- data/test/integration/mcp_integration_test.rb +4 -2
- data/test/integration/standalone_json_report_test.rb +46 -0
- data/test/integration/without_redis_test.rb +76 -0
- data/test/rails7_dummy/config/application.rb +1 -0
- data/test/rails7_dummy/config/coverband_missing_redis.rb +0 -2
- data/test/rails7_dummy/config/coverband_rails_cache.rb +16 -0
- data/test/rails8_dummy/config/application.rb +1 -0
- data/test/rails8_dummy/config/coverband_rails_cache.rb +3 -0
- data/test/rails_test_helper.rb +2 -1
- data/test/test_helper.rb +5 -17
- data/views/file_list.erb +2 -2
- data/views/source_file.erb +4 -3
- metadata +48 -40
- data/lib/coverband/adapters/memcached_store.rb +0 -85
- data/lib/coverband/adapters/web_service_store.rb +0 -152
- data/lib/coverband/collectors/view_tracker_service.rb +0 -59
- data/test/coverband/adapters/memecached_store_test.rb +0 -26
- 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
|
|
66
|
+
return {} unless storage
|
|
57
67
|
|
|
58
|
-
|
|
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
|
|
92
|
+
return "N/A" unless storage
|
|
80
93
|
|
|
81
|
-
|
|
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
|
|
101
|
+
return unless storage
|
|
90
102
|
|
|
91
|
-
|
|
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
|
|
108
|
+
return unless storage
|
|
98
109
|
|
|
99
|
-
|
|
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
|
|
141
|
+
return unless storage
|
|
106
142
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
"#{class_key}_tracker"
|
|
170
|
-
end
|
|
258
|
+
private
|
|
171
259
|
|
|
172
|
-
def
|
|
173
|
-
"
|
|
260
|
+
def concrete_target
|
|
261
|
+
raise "subclass must implement"
|
|
174
262
|
end
|
|
175
263
|
|
|
176
264
|
def class_key
|
|
177
|
-
@class_key ||=
|
|
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
|
|
56
|
+
return {} unless storage
|
|
57
57
|
|
|
58
|
-
|
|
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
|
|
65
|
+
return {} unless storage
|
|
67
66
|
|
|
68
|
-
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
)
|
|
@@ -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
|
|
@@ -68,11 +68,10 @@ module Coverband
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def used_keys
|
|
71
|
-
return {} unless
|
|
71
|
+
return {} unless storage
|
|
72
72
|
|
|
73
|
-
views = redis_store.hgetall(tracker_key)
|
|
74
73
|
normalized_views = {}
|
|
75
|
-
|
|
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
|
|
104
|
+
return unless storage
|
|
106
105
|
|
|
107
106
|
filename = "#{@project_directory}/#{filename}"
|
|
108
|
-
|
|
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
|
+
)
|