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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eda71f5ba3b7d7c1aa9a1c622a29e4329b8ab08bccc53d243aabea3a80a70cb
4
- data.tar.gz: 213fa97a6ceede04c3f37160defb2a6732d84a32f25681a9486a567eae6d7fed
3
+ metadata.gz: 60dd6c4c552576ae19c4cb2a84232c5f7e6b83b0d8fb8e11b3a98bc5abf69bdf
4
+ data.tar.gz: f9f638c079489229bed7e525caaed0fb995b19ceff250146644d405462739e39
5
5
  SHA512:
6
- metadata.gz: d4b4cf6feb4d736114f7324a0b34a65e32ee7f3edd610ee5aabb41daa069fd2417a746e927761c28ec22b37a9ceb1f768c2fa92064c11e0d4e4474801dd9f008
7
- data.tar.gz: 9e6641fc8b17e5d4d2a5d5d7981afecca83feffbc549d88206105a4f752feeb951290fa9dc77f8d24166519df12da917d83f81685eb2d79b6ca87912d09ff599
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 'simplecov'
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 'rack'
31
- gem 'webrick'
30
+ gem "rack"
31
+ gem "webrick"
32
32
 
33
- gem 'slim' # not used in production, for compiling templates
33
+ gem "slim" # not used in production, for compiling templates
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
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 'slim'
18
+ require "slim"
18
19
  # Slim::Template.new { '.lala' }.precompiled_template
19
- Dir['lib/deprecation_collector/web/views/*.slim'].each do |file|
20
- target = file.sub(/\.slim\z/, '.template.rb')
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['lib/deprecation_collector/web/views/*.slim'].map { |template| template.sub(/\.slim\z/, '.template.rb') }
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"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeprecationCollector
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -1,22 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'router'
4
- require_relative 'helpers'
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
- unless defined?(Temple::Utils) || ENV['DEPRECATION_COLLECTOR_RELOAD_WEB_TEMPLATES']
17
- # used for escaping in compiled slim templates
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] == '0'
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 '/dump.json' do
40
+ get "/dump.json" do
42
41
  render json: collector_instance.dump
43
42
  end
44
43
 
45
- get '/import' do
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 '/import' do
52
- halt 422, "need multipart json file" unless env['CONTENT_TYPE']&.start_with?('multipart/form-data') && params.dig(:file, :tempfile)
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 '/:id' do # show
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 '/all' do
70
+ delete "/all" do
63
71
  collector_instance.flush_redis
64
72
  redirect_to deprecations_path
65
73
  end
66
74
 
67
- post '/enable' do
75
+ post "/enable" do
68
76
  collector_instance.enable
69
77
  redirect_to deprecations_path
70
78
  end
71
79
 
72
- delete '/disable' do
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 '/:id' do # destroy
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 '/trigger' do # trigger
84
- trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?('2.7')
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
- "#{root_path}"
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 'dark' if params['dark']
56
- return 'light' if params['light']
57
- return 'dark' if request.get_header('HTTP_Sec_CH_Prefers_Color_Scheme').to_s.downcase.include?("dark")
58
- 'auto'
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
- {}.tap do |tags|
63
- tags[:kwargs] = 'bg-secondary' if deprecation[:message].include?("Using the last argument as keyword parameters is deprecated") ||
64
- deprecation[:message].include?("Passing the keyword argument as the last hash parameter is deprecated")
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
- tags[:test] = 'bg-success' if deprecation[:message].include?("trigger_kwargs_error_warning") ||
67
- deprecation[:message].include?("trigger_rails_deprecation")
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
- deprecation.dig(:notes, :tags)&.each { |tag| tags[tag] = 'bg-secondary' }
86
+ [tag, "bg-secondary"]
72
87
  end
73
88
  end
74
89
  end