deprecation_collector 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -4
- data/Gemfile.lock +1 -1
- data/Rakefile +5 -4
- data/deprecation_collector.gemspec +1 -1
- data/gemfiles/rails_6.gemfile.lock +1 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_none.gemfile.lock +1 -1
- data/lib/deprecation_collector/deprecation.rb +2 -1
- data/lib/deprecation_collector/storage.rb +212 -0
- data/lib/deprecation_collector/version.rb +1 -1
- data/lib/deprecation_collector/web/application.rb +27 -19
- data/lib/deprecation_collector/web/helpers.rb +31 -16
- data/lib/deprecation_collector/web/router.rb +69 -52
- data/lib/deprecation_collector/web/utils.rb +10 -7
- data/lib/deprecation_collector/web/views/index.html.template.rb +18 -7
- data/lib/deprecation_collector/web/views/show.html.template.rb +72 -3
- data/lib/deprecation_collector/web.rb +3 -1
- data/lib/deprecation_collector.rb +85 -118
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60dd6c4c552576ae19c4cb2a84232c5f7e6b83b0d8fb8e11b3a98bc5abf69bdf
|
4
|
+
data.tar.gz: f9f638c079489229bed7e525caaed0fb995b19ceff250146644d405462739e39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b80b9bcfdbbccaf112b447f3ab04960f042ce98c74b53b35b76e9c4f3740a37fa1cc894ce632e7206c76bcb7eaaf8e13f596bcaa53398eb611fcd245a8b77214
|
7
|
+
data.tar.gz: 35992cd452d01b83a5884299873bd2d80c2e5d42f8977d77b72e0635f24d5bda475200eed936e74102181205575d62e511085606af3868b059377780e9efc8e1
|
data/.rubocop.yml
CHANGED
@@ -10,6 +10,7 @@ AllCops:
|
|
10
10
|
SuggestExtensions: false
|
11
11
|
Exclude:
|
12
12
|
- gemfiles/*
|
13
|
+
- lib/deprecation_collector/web/views/*.template.rb
|
13
14
|
|
14
15
|
Style/StringLiterals:
|
15
16
|
Enabled: true
|
@@ -30,3 +31,4 @@ Metrics/PerceivedComplexity: { Max: 9 }
|
|
30
31
|
|
31
32
|
RSpec/ExampleLength: { Enabled: false }
|
32
33
|
RSpec/MultipleExpectations: { Enabled: false }
|
34
|
+
RSpec/MessageSpies: { Enabled: false }
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.5.0
|
2
|
+
- more work on ui
|
3
|
+
- refactored to separate deprecations storage from other logic
|
4
|
+
- when redis is not provided - print all messages to stderr
|
5
|
+
- added `key_prefix` option (default `'deprecations'`, location may change in the future) to allow multiple independent apps to write to one redis
|
6
|
+
- added `app_name` option to record app name as separate field
|
7
|
+
|
1
8
|
== 0.4.0
|
2
9
|
- a bit better ui
|
3
10
|
- simple import/export
|
data/Gemfile
CHANGED
@@ -20,14 +20,14 @@ unless defined?(Appraisal)
|
|
20
20
|
end
|
21
21
|
|
22
22
|
gem "rails", "~>6.0.0"
|
23
|
-
gem
|
23
|
+
gem "simplecov"
|
24
24
|
end
|
25
25
|
|
26
26
|
gem "fakeredis"
|
27
27
|
gem "redis", "~>4.8"
|
28
28
|
|
29
29
|
# for web tests
|
30
|
-
gem
|
31
|
-
gem
|
30
|
+
gem "rack"
|
31
|
+
gem "webrick"
|
32
32
|
|
33
|
-
gem
|
33
|
+
gem "slim" # not used in production, for compiling templates
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
@@ -8,16 +8,17 @@ RSpec::Core::RakeTask.new(:spec)
|
|
8
8
|
begin
|
9
9
|
require "rubocop/rake_task"
|
10
10
|
RuboCop::RakeTask.new
|
11
|
-
rescue LoadError
|
11
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
12
12
|
end
|
13
13
|
|
14
14
|
task default: %i[spec rubocop]
|
15
15
|
|
16
|
+
desc "Compile slim templates (so that slim is not needed as dependency)"
|
16
17
|
task :precompile_templates do
|
17
|
-
require
|
18
|
+
require "slim"
|
18
19
|
# Slim::Template.new { '.lala' }.precompiled_template
|
19
|
-
Dir[
|
20
|
-
target = file.sub(/\.slim\z/,
|
20
|
+
Dir["lib/deprecation_collector/web/views/*.slim"].each do |file|
|
21
|
+
target = file.sub(/\.slim\z/, ".template.rb")
|
21
22
|
puts "Compiling #{file} -> #{target}"
|
22
23
|
content = Slim::Template.new(file).precompiled_template # maybe send(:precompiled, []) is more correct
|
23
24
|
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) ||
|
26
26
|
f.match(%r{\Alib/deprecation_collector/web/views/.+\.slim\z})
|
27
27
|
end
|
28
|
-
end + Dir[
|
28
|
+
end + Dir["lib/deprecation_collector/web/views/*.slim"].map { |template| template.sub(/\.slim\z/, ".template.rb") }
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
31
|
spec.add_dependency "redis", ">= 3.0"
|
@@ -4,7 +4,7 @@ class DeprecationCollector
|
|
4
4
|
# :nodoc:
|
5
5
|
class Deprecation
|
6
6
|
attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :first_timestamp, :full_backtrace
|
7
|
-
attr_accessor :context, :custom_fingerprint
|
7
|
+
attr_accessor :context, :custom_fingerprint, :app_name
|
8
8
|
|
9
9
|
CLEANUP_REGEXES = {
|
10
10
|
# rails views generated methods names are unique per-worker
|
@@ -61,6 +61,7 @@ class DeprecationCollector
|
|
61
61
|
|
62
62
|
def as_json(_options = {})
|
63
63
|
{
|
64
|
+
app: app_name,
|
64
65
|
message: message,
|
65
66
|
realm: realm,
|
66
67
|
app_traceline: app_traceline,
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis"
|
4
|
+
|
5
|
+
class DeprecationCollector
|
6
|
+
module Storage
|
7
|
+
# :nodoc:
|
8
|
+
class Base
|
9
|
+
# rubocop:disable Style/SingleLineMethods
|
10
|
+
def initialize(**); end
|
11
|
+
def support_disabling?; false; end
|
12
|
+
def enabled?; true; end
|
13
|
+
def enable; end
|
14
|
+
def disable; end
|
15
|
+
|
16
|
+
def unsent_deprecations; []; end
|
17
|
+
def fetch_known_digests; end
|
18
|
+
|
19
|
+
def delete(digests); end
|
20
|
+
def clear(enable: false); end
|
21
|
+
def flush(**); end
|
22
|
+
|
23
|
+
def store(_deprecation); raise("Not implemented"); end
|
24
|
+
# rubocop:enable Style/SingleLineMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
# dummy strategy that outputs every deprecation into stderr
|
28
|
+
class StdErr < Base
|
29
|
+
def store(deprecation)
|
30
|
+
DeprecationCollector.instance.send(:log_deprecation, deprecation)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# storing in redis with deduplication by fingerprint
|
35
|
+
class Redis < Base
|
36
|
+
attr_accessor :write_interval, :write_interval_jitter, :redis, :count
|
37
|
+
|
38
|
+
def initialize(redis: nil, mutex: nil, count: false, write_interval: 900, write_interval_jitter: 60,
|
39
|
+
key_prefix: nil)
|
40
|
+
super
|
41
|
+
@key_prefix = key_prefix || "deprecations"
|
42
|
+
@redis = redis
|
43
|
+
@last_write_time = current_time
|
44
|
+
@count = count
|
45
|
+
@write_interval = write_interval
|
46
|
+
@write_interval_jitter = write_interval_jitter
|
47
|
+
# on cruby hash itself is threadsafe, but we need to prevent races
|
48
|
+
@deprecations_mutex = mutex || Mutex.new
|
49
|
+
@deprecations = {}
|
50
|
+
@known_digests = Set.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def support_disabling?
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def unsent_deprecations
|
58
|
+
@deprecations
|
59
|
+
end
|
60
|
+
|
61
|
+
def enabled?
|
62
|
+
@redis.get(enabled_flag_key) != "false"
|
63
|
+
end
|
64
|
+
|
65
|
+
def enable
|
66
|
+
@redis.set(enabled_flag_key, "true")
|
67
|
+
end
|
68
|
+
|
69
|
+
def disable
|
70
|
+
@redis.set(enabled_flag_key, "false")
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete(remove_digests)
|
74
|
+
return 0 unless remove_digests.any?
|
75
|
+
|
76
|
+
@redis.pipelined do |pipe|
|
77
|
+
pipe.hdel(data_hash_key, *remove_digests)
|
78
|
+
pipe.hdel(notes_hash_key, *remove_digests)
|
79
|
+
pipe.hdel(counter_hash_key, *remove_digests) if @count
|
80
|
+
end.first
|
81
|
+
end
|
82
|
+
|
83
|
+
def clear(enable: false)
|
84
|
+
@redis.del(data_hash_key, counter_hash_key, notes_hash_key)
|
85
|
+
@redis.del(enabled_flag_key) if enable
|
86
|
+
@known_digests.clear
|
87
|
+
@deprecations.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_known_digests
|
91
|
+
# FIXME: use `.merge!`?
|
92
|
+
@known_digests.merge(@redis.hkeys(data_hash_key))
|
93
|
+
end
|
94
|
+
|
95
|
+
def store(deprecation)
|
96
|
+
fresh = !@deprecations.key?(deprecation.digest)
|
97
|
+
@deprecations_mutex.synchronize do
|
98
|
+
(@deprecations[deprecation.digest] ||= deprecation).touch
|
99
|
+
end
|
100
|
+
|
101
|
+
flush if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
|
102
|
+
fresh
|
103
|
+
end
|
104
|
+
|
105
|
+
def flush(force: false)
|
106
|
+
return unless force || (current_time > @last_write_time + @write_interval)
|
107
|
+
|
108
|
+
deprecations_to_flush = nil
|
109
|
+
@deprecations_mutex.synchronize do
|
110
|
+
deprecations_to_flush = @deprecations
|
111
|
+
@deprecations = {}
|
112
|
+
@last_write_time = current_time
|
113
|
+
# checking in this section to prevent multiple parallel check requests
|
114
|
+
return DeprecationCollector.instance.instance_variable_set(:@enabled, false) unless enabled?
|
115
|
+
end
|
116
|
+
|
117
|
+
write_count_to_redis(deprecations_to_flush) if @count
|
118
|
+
|
119
|
+
# make as few writes as possible, other workers may already have reported our warning
|
120
|
+
fetch_known_digests
|
121
|
+
deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
|
122
|
+
return unless deprecations_to_flush.any?
|
123
|
+
|
124
|
+
@known_digests.merge(deprecations_to_flush.keys)
|
125
|
+
@redis.mapped_hmset(data_hash_key, deprecations_to_flush.transform_values(&:to_json))
|
126
|
+
end
|
127
|
+
|
128
|
+
def read_each
|
129
|
+
cursor = 0
|
130
|
+
loop do
|
131
|
+
cursor, data_pairs = @redis.hscan(data_hash_key, cursor)
|
132
|
+
|
133
|
+
if data_pairs.any?
|
134
|
+
data_pairs.zip(
|
135
|
+
@redis.hmget(counter_hash_key, data_pairs.map(&:first)),
|
136
|
+
@redis.hmget(notes_hash_key, data_pairs.map(&:first))
|
137
|
+
).each do |(digest, data), count, notes|
|
138
|
+
yield(digest, data, count, notes)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
break if cursor == "0"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def read_one(digest)
|
146
|
+
[
|
147
|
+
digest,
|
148
|
+
*@redis.pipelined do |pipe|
|
149
|
+
pipe.hget(data_hash_key, digest)
|
150
|
+
pipe.hget(counter_hash_key, digest)
|
151
|
+
pipe.hget(notes_hash_key, digest)
|
152
|
+
end
|
153
|
+
]
|
154
|
+
end
|
155
|
+
|
156
|
+
def import(dump_hash)
|
157
|
+
@redis.mapped_hmset(data_hash_key, dump_hash.transform_values(&:to_json))
|
158
|
+
end
|
159
|
+
|
160
|
+
def cleanup(&_block)
|
161
|
+
cursor = 0
|
162
|
+
removed = total = 0
|
163
|
+
loop do
|
164
|
+
cursor, data_pairs = @redis.hscan(data_hash_key, cursor) # NB: some pages may be empty
|
165
|
+
total += data_pairs.size
|
166
|
+
removed += delete(
|
167
|
+
data_pairs.to_h.select { |_digest, data| yield(JSON.parse(data, symbolize_names: true)) }.keys
|
168
|
+
)
|
169
|
+
break if cursor == "0"
|
170
|
+
end
|
171
|
+
"#{removed} removed, #{total - removed} left"
|
172
|
+
end
|
173
|
+
|
174
|
+
def key_prefix=(val)
|
175
|
+
@enabled_flag_key = @data_hash_key = @counter_hash_key = @notes_hash_key = nil
|
176
|
+
@key_prefix = val
|
177
|
+
end
|
178
|
+
|
179
|
+
protected
|
180
|
+
|
181
|
+
def enabled_flag_key
|
182
|
+
@enabled_flag_key ||= "#{@key_prefix}:enabled" # usually deprecations:enabled
|
183
|
+
end
|
184
|
+
|
185
|
+
def data_hash_key
|
186
|
+
@data_hash_key ||= "#{@key_prefix}:data" # usually deprecations:data
|
187
|
+
end
|
188
|
+
|
189
|
+
def counter_hash_key
|
190
|
+
@counter_hash_key ||= "#{@key_prefix}:counter" # usually deprecations:counter
|
191
|
+
end
|
192
|
+
|
193
|
+
def notes_hash_key
|
194
|
+
@notes_hash_key ||= "#{@key_prefix}:notes" # usually deprecations:notes
|
195
|
+
end
|
196
|
+
|
197
|
+
def current_time
|
198
|
+
return Time.zone.now if Time.respond_to?(:zone) && Time.zone
|
199
|
+
|
200
|
+
Time.now
|
201
|
+
end
|
202
|
+
|
203
|
+
def write_count_to_redis(deprecations_to_flush)
|
204
|
+
@redis.pipelined do |pipe|
|
205
|
+
deprecations_to_flush.each_pair do |digest, deprecation|
|
206
|
+
pipe.hincrby(counter_hash_key, digest, deprecation.occurences)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -1,22 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require 'pp'
|
3
|
+
require_relative "router"
|
4
|
+
require_relative "helpers"
|
6
5
|
|
7
6
|
class DeprecationCollector
|
8
7
|
class Web
|
8
|
+
# :nodoc:
|
9
9
|
class Application
|
10
10
|
extend Web::Router
|
11
11
|
helpers Helpers
|
12
12
|
|
13
13
|
attr_reader :web
|
14
|
+
|
14
15
|
def initialize(web)
|
15
16
|
@web = web
|
16
|
-
|
17
|
-
|
18
|
-
require_relative 'utils'
|
19
|
-
end
|
17
|
+
# used for escaping in compiled slim templates
|
18
|
+
require_relative "utils" unless defined?(Temple::Utils) || ENV["DEPRECATION_COLLECTOR_RELOAD_WEB_TEMPLATES"]
|
20
19
|
end
|
21
20
|
|
22
21
|
def call(env)
|
@@ -25,7 +24,7 @@ class DeprecationCollector
|
|
25
24
|
|
26
25
|
root do # index
|
27
26
|
@deprecations = collector_instance.read_each.to_a.compact
|
28
|
-
@deprecations = @deprecations.sort_by { |dep| dep[:message] } unless params[:sort] ==
|
27
|
+
@deprecations = @deprecations.sort_by { |dep| dep[:message] } unless params[:sort] == "0"
|
29
28
|
|
30
29
|
if params[:reject]
|
31
30
|
@deprecations = @deprecations.reject { |dep| dep[:message].match?(Regexp.union(Array(params[:reject]))) }
|
@@ -38,50 +37,59 @@ class DeprecationCollector
|
|
38
37
|
render slim: "index.html"
|
39
38
|
end
|
40
39
|
|
41
|
-
get
|
40
|
+
get "/dump.json" do
|
42
41
|
render json: collector_instance.dump
|
43
42
|
end
|
44
43
|
|
45
|
-
get
|
44
|
+
get "/import" do
|
46
45
|
return "Import not enabled" unless import_enabled?
|
47
46
|
|
48
47
|
render slim: "import.html"
|
49
48
|
end
|
50
49
|
|
51
|
-
post
|
52
|
-
|
50
|
+
post "/import" do
|
51
|
+
unless env["CONTENT_TYPE"]&.start_with?("multipart/form-data") && params.dig(:file, :tempfile)
|
52
|
+
halt 422, "need multipart json file"
|
53
|
+
end
|
53
54
|
collector_instance.import_dump(File.read(params[:file][:tempfile]))
|
54
55
|
redirect_to deprecations_path
|
55
56
|
end
|
56
57
|
|
57
|
-
get
|
58
|
+
get "/:id.json" do # show
|
59
|
+
@deprecation = collector_instance.read_one(params[:id])
|
60
|
+
halt 404 unless @deprecation
|
61
|
+
render json: JSON.pretty_generate(@deprecation)
|
62
|
+
end
|
63
|
+
|
64
|
+
get "/:id" do # show
|
58
65
|
@deprecation = collector_instance.read_one(params[:id])
|
66
|
+
halt 404 unless @deprecation
|
59
67
|
render slim: "show.html"
|
60
68
|
end
|
61
69
|
|
62
|
-
delete
|
70
|
+
delete "/all" do
|
63
71
|
collector_instance.flush_redis
|
64
72
|
redirect_to deprecations_path
|
65
73
|
end
|
66
74
|
|
67
|
-
post
|
75
|
+
post "/enable" do
|
68
76
|
collector_instance.enable
|
69
77
|
redirect_to deprecations_path
|
70
78
|
end
|
71
79
|
|
72
|
-
delete
|
80
|
+
delete "/disable" do
|
73
81
|
collector_instance.disable
|
74
82
|
redirect_to deprecations_path
|
75
83
|
end
|
76
84
|
|
77
85
|
# NB: order for wildcards is important
|
78
|
-
delete
|
86
|
+
delete "/:id" do # destroy
|
79
87
|
collector_instance.delete_deprecations([params[:id]])
|
80
88
|
redirect_to deprecations_path
|
81
89
|
end
|
82
90
|
|
83
|
-
post
|
84
|
-
trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?(
|
91
|
+
post "/trigger" do # trigger
|
92
|
+
trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?("2.7")
|
85
93
|
trigger_rails_deprecation
|
86
94
|
collector_instance.collect(
|
87
95
|
"TestFoo#assign_attributes called (test attr_spy) trigger_rails_deprecation", caller_locations, :attr_spy
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
class DeprecationCollector
|
4
4
|
class Web
|
5
|
+
# :nodoc:
|
5
6
|
module Helpers
|
6
7
|
def collector_instance
|
7
8
|
@collector_instance || DeprecationCollector.instance
|
@@ -17,15 +18,15 @@ class DeprecationCollector
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def current_path
|
20
|
-
@current_path ||= request.path_info.gsub(
|
21
|
+
@current_path ||= request.path_info.gsub(%r{^/}, "")
|
21
22
|
end
|
22
23
|
|
23
24
|
def deprecations_path
|
24
|
-
|
25
|
+
root_path # /
|
25
26
|
end
|
26
27
|
|
27
|
-
def deprecation_path(id)
|
28
|
-
"#{root_path}#{id}"
|
28
|
+
def deprecation_path(id, format: nil)
|
29
|
+
["#{root_path}#{id}", format].compact.join('.')
|
29
30
|
end
|
30
31
|
|
31
32
|
def enable_deprecations_path
|
@@ -48,27 +49,41 @@ class DeprecationCollector
|
|
48
49
|
|
49
50
|
def trigger_rails_deprecation
|
50
51
|
return unless defined?(ActiveSupport::Deprecation)
|
52
|
+
|
51
53
|
-> { ActiveSupport::Deprecation.warn("Test deprecation") } []
|
52
54
|
end
|
53
55
|
|
54
56
|
def current_color_theme
|
55
|
-
return
|
56
|
-
return
|
57
|
-
return
|
58
|
-
|
57
|
+
return "dark" if params["dark"]
|
58
|
+
return "light" if params["light"]
|
59
|
+
return "dark" if request.get_header("HTTP_Sec_CH_Prefers_Color_Scheme").to_s.downcase.include?("dark")
|
60
|
+
|
61
|
+
"auto"
|
62
|
+
end
|
63
|
+
|
64
|
+
def detect_tag(deprecation)
|
65
|
+
msg = deprecation[:message]
|
66
|
+
return :kwargs if msg.include?("Using the last argument as keyword parameters is deprecated") ||
|
67
|
+
msg.include?("Passing the keyword argument as the last hash parameter is deprecated")
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_deprecation?(deprecation)
|
71
|
+
%w[trigger_kwargs_error_warning trigger_rails_deprecation].any? { |method| deprecation[:message]}
|
59
72
|
end
|
60
73
|
|
61
74
|
def deprecation_tags(deprecation)
|
62
|
-
|
63
|
-
|
64
|
-
|
75
|
+
tags = Set.new
|
76
|
+
if (detected_tag = detect_tag(deprecation))
|
77
|
+
tags << detected_tag
|
78
|
+
end
|
79
|
+
tags << :test if test_deprecation?(deprecation)
|
80
|
+
tags << deprecation[:realm] if deprecation[:realm] && deprecation[:realm] != "rails"
|
81
|
+
tags.merge(deprecation.dig(:notes, :tags) || [])
|
65
82
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
tags[deprecation[:realm]] = 'bg-secondary' if deprecation[:realm] && deprecation[:realm] != 'rails'
|
83
|
+
tags.to_h do |tag|
|
84
|
+
next [tag, "bg-success"] if tag == :test
|
70
85
|
|
71
|
-
|
86
|
+
[tag, "bg-secondary"]
|
72
87
|
end
|
73
88
|
end
|
74
89
|
end
|