rollout 2.4.5 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acf240ab59dcfdebec335aee19df882f8936e7de5b7a0c347a3c28cfa8b3fc6d
4
- data.tar.gz: 2ca7c2d7c761c6e539790421547b5e8eb82bbe8b56dca246975f6adc0773866d
3
+ metadata.gz: 37e56080ba30fb08aaa7e718e22fae3cab0745e84a91537e4e5520c2843a63a2
4
+ data.tar.gz: 2b2f8c5a6e7a3ddd8275d818b4a168e7573204c2e259e323ca7a2ea9996062d9
5
5
  SHA512:
6
- metadata.gz: dc61fd463b60a94e85b5b141ad5fb7431f37249d9b899739b52524bd0fc28106135e039cbfe0130a0c35d0934b50339e426e38255516381636ec1ff226068e49
7
- data.tar.gz: 5babd80ccaab8b4f7b4efeac63e5941709913014e5d5d37f71c5f8b04ae62d09a38e50f7f6236df8d09ce359384cb0d877452209a1b63b69284fd649bb49033c
6
+ metadata.gz: a31905463a16ca7413566460c9a693ab5b9063e74547c05aafe2cf40d5e8f8d83ecb9e85316dd48c3e6b5e5b63311ac222343b485228a695ff5b72f8b0d82a2c
7
+ data.tar.gz: 33fc542bdfe0def2b13a1b3697ba59c1e5f83e813a7c67740d94bf0a9725e17003fdc3fecfa923874883730dde589e0906b5d49ab19ff49c36943e259c28f134
@@ -3,24 +3,33 @@ version: 2.1
3
3
  workflows:
4
4
  main:
5
5
  jobs:
6
+ - ruby27
6
7
  - ruby26
7
8
  - ruby25
8
9
  - ruby24
9
10
  - ruby23
10
11
 
11
12
  executors:
13
+ ruby27:
14
+ docker:
15
+ - image: circleci/ruby:2.7
16
+ - image: circleci/redis:alpine
12
17
  ruby26:
13
18
  docker:
14
19
  - image: circleci/ruby:2.6
20
+ - image: circleci/redis:alpine
15
21
  ruby25:
16
22
  docker:
17
23
  - image: circleci/ruby:2.5
24
+ - image: circleci/redis:alpine
18
25
  ruby24:
19
26
  docker:
20
27
  - image: circleci/ruby:2.4
28
+ - image: circleci/redis:alpine
21
29
  ruby23:
22
30
  docker:
23
31
  - image: circleci/ruby:2.3
32
+ - image: circleci/redis:alpine
24
33
 
25
34
  commands:
26
35
  test:
@@ -31,7 +40,7 @@ commands:
31
40
 
32
41
  - run:
33
42
  name: Bundle Install
34
- command: bundle check --path vendor/bundle || bundle install --deployment
43
+ command: bundle check --path vendor/bundle || bundle install
35
44
 
36
45
  - save_cache:
37
46
  key: bundler-{{ checksum "Gemfile.lock" }}
@@ -44,8 +53,8 @@ commands:
44
53
  bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
45
54
 
46
55
  jobs:
47
- ruby26:
48
- executor: ruby26
56
+ ruby27:
57
+ executor: ruby27
49
58
  steps:
50
59
  - checkout
51
60
  - test
@@ -61,6 +70,12 @@ jobs:
61
70
  - store_test_results:
62
71
  path: test_results
63
72
 
73
+ ruby26:
74
+ executor: ruby26
75
+ steps:
76
+ - checkout
77
+ - test
78
+
64
79
  ruby25:
65
80
  executor: ruby25
66
81
  steps:
data/README.md CHANGED
@@ -24,6 +24,14 @@ $redis = Redis.new
24
24
  $rollout = Rollout.new($redis)
25
25
  ```
26
26
 
27
+ or even simpler
28
+
29
+ ```ruby
30
+ require 'redis'
31
+ $rollout = Rollout.new(Redis.current) # Will use REDIS_URL env var or default redis url
32
+ ```
33
+
34
+
27
35
  Update data specific to a feature:
28
36
 
29
37
  ```ruby
@@ -1,144 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rollout/feature'
4
+ require 'rollout/logging'
3
5
  require 'rollout/version'
4
6
  require 'zlib'
5
7
  require 'set'
6
8
  require 'json'
9
+ require 'observer'
7
10
 
8
11
  class Rollout
9
- RAND_BASE = (2**32 - 1) / 100.0
10
-
11
- class Feature
12
- attr_accessor :groups, :users, :percentage, :data
13
- attr_reader :name, :options
14
-
15
- def initialize(name, string = nil, opts = {})
16
- @options = opts
17
- @name = name
18
-
19
- if string
20
- raw_percentage, raw_users, raw_groups, raw_data = string.split('|', 4)
21
- @percentage = raw_percentage.to_f
22
- @users = users_from_string(raw_users)
23
- @groups = groups_from_string(raw_groups)
24
- @data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
25
- else
26
- clear
27
- end
28
- end
29
-
30
- def serialize
31
- "#{@percentage}|#{@users.to_a.join(',')}|#{@groups.to_a.join(',')}|#{serialize_data}"
32
- end
33
-
34
- def add_user(user)
35
- id = user_id(user)
36
- @users << id unless @users.include?(id)
37
- end
38
-
39
- def remove_user(user)
40
- @users.delete(user_id(user))
41
- end
42
-
43
- def add_group(group)
44
- @groups << group.to_sym unless @groups.include?(group.to_sym)
45
- end
46
-
47
- def remove_group(group)
48
- @groups.delete(group.to_sym)
49
- end
50
-
51
- def clear
52
- @groups = groups_from_string('')
53
- @users = users_from_string('')
54
- @percentage = 0
55
- @data = {}
56
- end
57
-
58
- def active?(rollout, user)
59
- if user
60
- id = user_id(user)
61
- user_in_percentage?(id) ||
62
- user_in_active_users?(id) ||
63
- user_in_active_group?(user, rollout)
64
- else
65
- @percentage == 100
66
- end
67
- end
68
-
69
- def user_in_active_users?(user)
70
- @users.include?(user_id(user))
71
- end
72
-
73
- def to_hash
74
- {
75
- percentage: @percentage,
76
- groups: @groups,
77
- users: @users
78
- }
79
- end
80
-
81
- private
82
-
83
- def user_id(user)
84
- if user.is_a?(Integer) || user.is_a?(String)
85
- user.to_s
86
- else
87
- user.send(id_user_by).to_s
88
- end
89
- end
90
-
91
- def id_user_by
92
- @options[:id_user_by] || :id
93
- end
94
-
95
- def user_in_percentage?(user)
96
- Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
97
- end
98
-
99
- def user_id_for_percentage(user)
100
- if @options[:randomize_percentage]
101
- user_id(user).to_s + @name.to_s
102
- else
103
- user_id(user)
104
- end
105
- end
106
-
107
- def user_in_active_group?(user, rollout)
108
- @groups.any? do |g|
109
- rollout.active_in_group?(g, user)
110
- end
111
- end
112
-
113
- def serialize_data
114
- return '' unless @data.is_a? Hash
12
+ include Observable
115
13
 
116
- @data.to_json
117
- end
14
+ RAND_BASE = (2**32 - 1) / 100.0
118
15
 
119
- def users_from_string(raw_users)
120
- users = (raw_users || '').split(',').map(&:to_s)
121
- if @options[:use_sets]
122
- users.to_set
123
- else
124
- users
125
- end
126
- end
127
-
128
- def groups_from_string(raw_groups)
129
- groups = (raw_groups || '').split(',').map(&:to_sym)
130
- if @options[:use_sets]
131
- groups.to_set
132
- else
133
- groups
134
- end
135
- end
136
- end
16
+ attr_reader :options, :storage
137
17
 
138
18
  def initialize(storage, opts = {})
139
19
  @storage = storage
140
20
  @options = opts
141
21
  @groups = { all: ->(_user) { true } }
22
+
23
+ extend(Logging) if opts[:logging]
24
+ end
25
+
26
+ def groups
27
+ @groups.keys
142
28
  end
143
29
 
144
30
  def activate(feature)
@@ -156,6 +42,10 @@ class Rollout
156
42
  features.delete(feature.to_s)
157
43
  @storage.set(features_key, features.join(','))
158
44
  @storage.del(key(feature))
45
+
46
+ if respond_to?(:logging)
47
+ logging.delete(feature)
48
+ end
159
49
  end
160
50
 
161
51
  def set(feature, desired_state)
@@ -296,7 +186,28 @@ class Rollout
296
186
  end
297
187
 
298
188
  def exists?(feature)
299
- @storage.exists(key(feature))
189
+ # since redis-rb v4.2, `#exists?` replaces `#exists` which now returns integer value instead of boolean
190
+ # https://github.com/redis/redis-rb/pull/918
191
+ if @storage.respond_to?(:exists?)
192
+ @storage.exists?(key(feature))
193
+ else
194
+ @storage.exists(key(feature))
195
+ end
196
+ end
197
+
198
+ def with_feature(feature)
199
+ f = get(feature)
200
+
201
+ if count_observers > 0
202
+ before = Marshal.load(Marshal.dump(f))
203
+ yield(f)
204
+ save(f)
205
+ changed
206
+ notify_observers(:update, before, f)
207
+ else
208
+ yield(f)
209
+ save(f)
210
+ end
300
211
  end
301
212
 
302
213
  private
@@ -309,12 +220,6 @@ class Rollout
309
220
  'feature:__features__'
310
221
  end
311
222
 
312
- def with_feature(feature)
313
- f = get(feature)
314
- yield(f)
315
- save(f)
316
- end
317
-
318
223
  def save(feature)
319
224
  @storage.set(key(feature.name), feature.serialize)
320
225
  @storage.set(features_key, (features | [feature.name.to_sym]).join(','))
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rollout
4
+ class Feature
5
+ attr_accessor :groups, :users, :percentage, :data
6
+ attr_reader :name, :options
7
+
8
+ def initialize(name, string = nil, opts = {})
9
+ @options = opts
10
+ @name = name
11
+
12
+ if string
13
+ raw_percentage, raw_users, raw_groups, raw_data = string.split('|', 4)
14
+ @percentage = raw_percentage.to_f
15
+ @users = users_from_string(raw_users)
16
+ @groups = groups_from_string(raw_groups)
17
+ @data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data)
18
+ else
19
+ clear
20
+ end
21
+ end
22
+
23
+ def serialize
24
+ "#{@percentage}|#{@users.to_a.join(',')}|#{@groups.to_a.join(',')}|#{serialize_data}"
25
+ end
26
+
27
+ def add_user(user)
28
+ id = user_id(user)
29
+ @users << id unless @users.include?(id)
30
+ end
31
+
32
+ def remove_user(user)
33
+ @users.delete(user_id(user))
34
+ end
35
+
36
+ def add_group(group)
37
+ @groups << group.to_sym unless @groups.include?(group.to_sym)
38
+ end
39
+
40
+ def remove_group(group)
41
+ @groups.delete(group.to_sym)
42
+ end
43
+
44
+ def clear
45
+ @groups = groups_from_string('')
46
+ @users = users_from_string('')
47
+ @percentage = 0
48
+ @data = {}
49
+ end
50
+
51
+ def active?(rollout, user)
52
+ if user
53
+ id = user_id(user)
54
+ user_in_percentage?(id) ||
55
+ user_in_active_users?(id) ||
56
+ user_in_active_group?(user, rollout)
57
+ else
58
+ @percentage == 100
59
+ end
60
+ end
61
+
62
+ def user_in_active_users?(user)
63
+ @users.include?(user_id(user))
64
+ end
65
+
66
+ def to_hash
67
+ {
68
+ percentage: @percentage,
69
+ groups: @groups,
70
+ users: @users,
71
+ data: @data,
72
+ }
73
+ end
74
+
75
+ private
76
+
77
+ def user_id(user)
78
+ if user.is_a?(Integer) || user.is_a?(String)
79
+ user.to_s
80
+ else
81
+ user.send(id_user_by).to_s
82
+ end
83
+ end
84
+
85
+ def id_user_by
86
+ @options[:id_user_by] || :id
87
+ end
88
+
89
+ def user_in_percentage?(user)
90
+ Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage
91
+ end
92
+
93
+ def user_id_for_percentage(user)
94
+ if @options[:randomize_percentage]
95
+ user_id(user).to_s + @name.to_s
96
+ else
97
+ user_id(user)
98
+ end
99
+ end
100
+
101
+ def user_in_active_group?(user, rollout)
102
+ @groups.any? do |g|
103
+ rollout.active_in_group?(g, user)
104
+ end
105
+ end
106
+
107
+ def serialize_data
108
+ return '' unless @data.is_a? Hash
109
+
110
+ @data.to_json
111
+ end
112
+
113
+ def users_from_string(raw_users)
114
+ users = (raw_users || '').split(',').map(&:to_s)
115
+ if @options[:use_sets]
116
+ users.to_set
117
+ else
118
+ users
119
+ end
120
+ end
121
+
122
+ def groups_from_string(raw_groups)
123
+ groups = (raw_groups || '').split(',').map(&:to_sym)
124
+ if @options[:use_sets]
125
+ groups.to_set
126
+ else
127
+ groups
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,198 @@
1
+ class Rollout
2
+ module Logging
3
+ def self.extended(rollout)
4
+ options = rollout.options[:logging]
5
+ options = options.is_a?(Hash) ? options.dup : {}
6
+ options[:storage] ||= rollout.storage
7
+
8
+ logger = Logger.new(**options)
9
+
10
+ rollout.add_observer(logger, :log)
11
+ rollout.define_singleton_method(:logging) do
12
+ logger
13
+ end
14
+ end
15
+
16
+ class Event
17
+ attr_reader :feature, :name, :data, :context, :created_at
18
+
19
+ def self.from_raw(value, score)
20
+ hash = JSON.parse(value, symbolize_names: true)
21
+
22
+ new(**hash.merge(created_at: Time.at(-score.to_f / 1_000_000)))
23
+ end
24
+
25
+ def initialize(feature: nil, name:, data:, context: {}, created_at:)
26
+ @feature = feature
27
+ @name = name
28
+ @data = data
29
+ @context = context
30
+ @created_at = created_at
31
+ end
32
+
33
+ def timestamp
34
+ (@created_at.to_f * 1_000_000).to_i
35
+ end
36
+
37
+ def serialize
38
+ JSON.dump(
39
+ feature: @feature,
40
+ name: @name,
41
+ data: @data,
42
+ context: @context,
43
+ )
44
+ end
45
+
46
+ def ==(other)
47
+ feature == other.feature \
48
+ && name == other.name \
49
+ && data == other.data \
50
+ && created_at == other.created_at
51
+ end
52
+ end
53
+
54
+ class Logger
55
+ def initialize(storage: nil, history_length: 50, global: false)
56
+ @history_length = history_length
57
+ @storage = storage
58
+ @global = global
59
+ end
60
+
61
+ def updated_at(feature_name)
62
+ storage_key = events_storage_key(feature_name)
63
+ _, score = @storage.zrange(storage_key, 0, 0, with_scores: true).first
64
+ Time.at(-score.to_f / 1_000_000) if score
65
+ end
66
+
67
+ def last_event(feature_name)
68
+ storage_key = events_storage_key(feature_name)
69
+ value = @storage.zrange(storage_key, 0, 0, with_scores: true).first
70
+ Event.from_raw(*value) if value
71
+ end
72
+
73
+ def events(feature_name)
74
+ storage_key = events_storage_key(feature_name)
75
+ @storage
76
+ .zrange(storage_key, 0, -1, with_scores: true)
77
+ .map { |v| Event.from_raw(*v) }
78
+ .reverse
79
+ end
80
+
81
+ def global_events
82
+ @storage
83
+ .zrange(global_events_storage_key, 0, -1, with_scores: true)
84
+ .map { |v| Event.from_raw(*v) }
85
+ .reverse
86
+ end
87
+
88
+ def delete(feature_name)
89
+ storage_key = events_storage_key(feature_name)
90
+ @storage.del(storage_key)
91
+ end
92
+
93
+ def update(before, after)
94
+ before_hash = before.to_hash
95
+ before_hash.delete(:data).each do |k, v|
96
+ before_hash["data.#{k}"] = v
97
+ end
98
+ after_hash = after.to_hash
99
+ after_hash.delete(:data).each do |k, v|
100
+ after_hash["data.#{k}"] = v
101
+ end
102
+
103
+ keys = before_hash.keys | after_hash.keys
104
+ change = { before: {}, after: {} }
105
+ changed_count = 0
106
+
107
+ keys.each do |key|
108
+ next if before_hash[key] == after_hash[key]
109
+
110
+ change[:before][key] = before_hash[key]
111
+ change[:after][key] = after_hash[key]
112
+
113
+ changed_count += 1
114
+ end
115
+
116
+ return if changed_count == 0
117
+
118
+ event = Event.new(
119
+ feature: after.name,
120
+ name: :update,
121
+ data: change,
122
+ context: current_context,
123
+ created_at: Time.now,
124
+ )
125
+
126
+ storage_key = events_storage_key(after.name)
127
+
128
+ @storage.zadd(storage_key, -event.timestamp, event.serialize)
129
+ @storage.zremrangebyrank(storage_key, @history_length, -1)
130
+
131
+ if @global
132
+ @storage.zadd(global_events_storage_key, -event.timestamp, event.serialize)
133
+ @storage.zremrangebyrank(global_events_storage_key, @history_length, -1)
134
+ end
135
+ end
136
+
137
+ def log(event, *args)
138
+ return unless logging_enabled?
139
+
140
+ unless respond_to?(event)
141
+ raise ArgumentError, "Invalid log event: #{event}"
142
+ end
143
+
144
+ expected_arity = method(event).arity
145
+ unless args.count == expected_arity
146
+ raise(
147
+ ArgumentError,
148
+ "Invalid number of arguments for event '#{event}': expected #{expected_arity} but got #{args.count}",
149
+ )
150
+ end
151
+
152
+ public_send(event, *args)
153
+ end
154
+
155
+ CONTEXT_THREAD_KEY = :rollout_logging_context
156
+ WITHOUT_THREAD_KEY = :rollout_logging_disabled
157
+
158
+ def with_context(context)
159
+ raise ArgumentError, "context must be a Hash" unless context.is_a?(Hash)
160
+ raise ArgumentError, "block is required" unless block_given?
161
+
162
+ Thread.current[CONTEXT_THREAD_KEY] = context
163
+ yield
164
+ ensure
165
+ Thread.current[CONTEXT_THREAD_KEY] = nil
166
+ end
167
+
168
+ def current_context
169
+ Thread.current[CONTEXT_THREAD_KEY] || {}
170
+ end
171
+
172
+ def without
173
+ Thread.current[WITHOUT_THREAD_KEY] = true
174
+ yield
175
+ ensure
176
+ Thread.current[WITHOUT_THREAD_KEY] = nil
177
+ end
178
+
179
+ def logging_enabled?
180
+ !Thread.current[WITHOUT_THREAD_KEY]
181
+ end
182
+
183
+ private
184
+
185
+ def global_events_storage_key
186
+ "feature:_global_:logging:events"
187
+ end
188
+
189
+ def events_storage_key(feature_name)
190
+ "feature:#{feature_name}:logging:events"
191
+ end
192
+
193
+ def current_timestamp
194
+ (Time.now.to_f * 1_000_000).to_i
195
+ end
196
+ end
197
+ end
198
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rollout
4
- VERSION = '2.4.5'
4
+ VERSION = '2.5.0'
5
5
  end
@@ -22,10 +22,10 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_dependency 'redis', '~> 4.0'
24
24
 
25
- spec.add_development_dependency 'bundler', '~> 1.17'
26
- spec.add_development_dependency 'fakeredis'
25
+ spec.add_development_dependency 'bundler', '>= 1.17'
26
+ spec.add_development_dependency 'pry'
27
27
  spec.add_development_dependency 'rspec', '~> 3.0'
28
28
  spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4'
29
29
  spec.add_development_dependency 'rubocop', '~> 0.71'
30
- spec.add_development_dependency 'simplecov', '~> 0.16'
30
+ spec.add_development_dependency 'simplecov', '0.17'
31
31
  end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Rollout::Logging' do
4
+ let(:rollout) { Rollout.new(Redis.current, logging: logging) }
5
+ let(:logging) { true }
6
+ let(:feature) { :foo }
7
+
8
+ it 'logs changes' do
9
+ expect(rollout.logging.last_event(feature)).to be_nil
10
+
11
+ rollout.activate_percentage(feature, 50)
12
+
13
+ expect(rollout.logging.updated_at(feature)).to_not be_nil
14
+
15
+ first_event = rollout.logging.last_event(feature)
16
+
17
+ expect(first_event.name).to eq 'update'
18
+ expect(first_event.data).to eq(before: { percentage: 0 }, after: { percentage: 50 })
19
+
20
+ rollout.activate_percentage(feature, 75)
21
+
22
+ second_event = rollout.logging.last_event(feature)
23
+
24
+ expect(second_event.name).to eq 'update'
25
+ expect(second_event.data).to eq(before: { percentage: 50 }, after: { percentage: 75 })
26
+
27
+ rollout.activate_group(feature, :hipsters)
28
+
29
+ third_event = rollout.logging.last_event(feature)
30
+
31
+ expect(third_event.name).to eq 'update'
32
+ expect(third_event.data).to eq(before: { groups: [] }, after: { groups: ['hipsters'] })
33
+
34
+ expect(rollout.logging.events(feature)).to eq [first_event, second_event, third_event]
35
+ end
36
+
37
+ context 'logging data changes' do
38
+ it 'logs changes' do
39
+ expect(rollout.logging.last_event(feature)).to be_nil
40
+
41
+ rollout.set_feature_data(feature, description: "foo")
42
+
43
+ event = rollout.logging.last_event(feature)
44
+
45
+ expect(event).not_to be_nil
46
+ expect(event.name).to eq 'update'
47
+ expect(event.data).to eq(before: { "data.description": nil }, after: { "data.description": "foo" })
48
+ end
49
+ end
50
+
51
+ context 'no logging' do
52
+ let(:logging) { nil }
53
+
54
+ it 'doesnt even respond to logging' do
55
+ expect(rollout).not_to respond_to :logging
56
+ end
57
+ end
58
+
59
+ context 'history truncation' do
60
+ let(:logging) { { history_length: 1 } }
61
+
62
+ it 'logs changes' do
63
+ expect(rollout.logging.last_event(feature)).to be_nil
64
+
65
+ rollout.activate_percentage(feature, 25)
66
+
67
+ first_event = rollout.logging.last_event(feature)
68
+
69
+ expect(first_event.name).to eq 'update'
70
+ expect(first_event.data).to eq(before: { percentage: 0 }, after: { percentage: 25 })
71
+
72
+ rollout.activate_percentage(feature, 30)
73
+
74
+ second_event = rollout.logging.last_event(feature)
75
+
76
+ expect(second_event.name).to eq 'update'
77
+ expect(second_event.data).to eq(before: { percentage: 25 }, after: { percentage: 30 })
78
+
79
+ expect(rollout.logging.events(feature)).to eq [second_event]
80
+ end
81
+ end
82
+
83
+ context 'with context' do
84
+ let(:current_user) { double(nickname: 'lester') }
85
+
86
+ it 'adds context to the event' do
87
+ rollout.logging.with_context(actor: current_user.nickname) do
88
+ rollout.activate_percentage(feature, 25)
89
+ end
90
+
91
+ event = rollout.logging.last_event(feature)
92
+
93
+ expect(event.name).to eq 'update'
94
+ expect(event.data).to eq(before: { percentage: 0 }, after: { percentage: 25 })
95
+ expect(event.context).to eq(actor: current_user.nickname)
96
+ end
97
+ end
98
+
99
+ context 'global logs' do
100
+ let(:logging) { { global: true } }
101
+ let(:feature_foo) { 'foo' }
102
+ let(:feature_bar) { 'bar' }
103
+
104
+ it 'logs changes' do
105
+ expect(rollout.logging.last_event(feature_foo)).to be_nil
106
+
107
+ rollout.activate_percentage(feature_foo, 25)
108
+
109
+ event_foo = rollout.logging.last_event(feature_foo)
110
+
111
+ expect(event_foo.feature).to eq feature_foo
112
+ expect(event_foo.name).to eq 'update'
113
+ expect(event_foo.data).to eq(before: { percentage: 0 }, after: { percentage: 25 })
114
+
115
+ expect(rollout.logging.events(feature_foo)).to eq [event_foo]
116
+
117
+ rollout.activate_percentage(feature_bar, 30)
118
+
119
+ event_bar = rollout.logging.last_event(feature_bar)
120
+
121
+ expect(event_bar.feature).to eq feature_bar
122
+ expect(event_bar.name).to eq 'update'
123
+ expect(event_bar.data).to eq(before: { percentage: 0 }, after: { percentage: 30 })
124
+
125
+ expect(rollout.logging.events(feature_bar)).to eq [event_bar]
126
+
127
+ expect(rollout.logging.global_events).to eq [event_foo, event_bar]
128
+ end
129
+ end
130
+
131
+ context 'no logging for block' do
132
+ it 'doesnt log' do
133
+ rollout.logging.without do
134
+ rollout.activate_percentage(feature, 25)
135
+ end
136
+
137
+ event = rollout.logging.last_event(feature)
138
+
139
+ expect(event).to be_nil
140
+ end
141
+ end
142
+ end
143
+
@@ -2,8 +2,7 @@ require "spec_helper"
2
2
 
3
3
  RSpec.describe "Rollout" do
4
4
  before do
5
- @redis = Redis.new
6
- @rollout = Rollout.new(@redis)
5
+ @rollout = Rollout.new(Redis.current)
7
6
  end
8
7
 
9
8
  describe "when a group is activated" do
@@ -430,7 +429,8 @@ RSpec.describe "Rollout" do
430
429
  expect(feature.to_hash).to eq(
431
430
  groups: [:caretakers, :greeters],
432
431
  percentage: 10,
433
- users: %w(42)
432
+ users: %w(42),
433
+ data: {},
434
434
  )
435
435
 
436
436
  feature = @rollout.get(:signup)
@@ -450,7 +450,8 @@ RSpec.describe "Rollout" do
450
450
  expect(feature.to_hash).to eq(
451
451
  groups: [:caretakers, :greeters].to_set,
452
452
  percentage: 10,
453
- users: %w(42).to_set
453
+ users: %w(42).to_set,
454
+ data: {},
454
455
  )
455
456
 
456
457
  feature = @rollout.get(:signup)
@@ -474,7 +475,8 @@ RSpec.describe "Rollout" do
474
475
  expect(@rollout.get(feature).to_hash).to eq(
475
476
  percentage: 0,
476
477
  users: [],
477
- groups: []
478
+ groups: [],
479
+ data: {},
478
480
  )
479
481
  end
480
482
  end
@@ -486,7 +488,8 @@ RSpec.describe "Rollout" do
486
488
  expect(@rollout.get(feature).to_hash).to eq(
487
489
  percentage: 0,
488
490
  users: Set.new,
489
- groups: Set.new
491
+ groups: Set.new,
492
+ data: {},
490
493
  )
491
494
  end
492
495
  end
@@ -5,8 +5,14 @@ require 'simplecov'
5
5
  SimpleCov.start
6
6
 
7
7
  require 'bundler/setup'
8
- require ENV["USE_REAL_REDIS"] == "true" ? "redis" : "fakeredis"
9
- require "rollout"
8
+ require 'redis'
9
+ require 'rollout'
10
+
11
+ Redis.current = Redis.new(
12
+ host: ENV.fetch('REDIS_HOST', '127.0.0.1'),
13
+ port: ENV.fetch('REDIS_PORT', '6379'),
14
+ db: ENV.fetch('REDIS_DB', '7'),
15
+ )
10
16
 
11
17
  RSpec.configure do |config|
12
18
  config.example_status_persistence_file_path = '.rspec_status'
@@ -17,5 +23,5 @@ RSpec.configure do |config|
17
23
  c.syntax = :expect
18
24
  end
19
25
 
20
- config.before { Redis.new.flushdb }
26
+ config.before { Redis.current.flushdb }
21
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.5
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Golick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-07 00:00:00.000000000 Z
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -28,18 +28,18 @@ dependencies:
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.17'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.17'
41
41
  - !ruby/object:Gem::Dependency
42
- name: fakeredis
42
+ name: pry
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: simplecov
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - '='
102
102
  - !ruby/object:Gem::Version
103
- version: '0.16'
103
+ version: '0.17'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - '='
109
109
  - !ruby/object:Gem::Version
110
- version: '0.16'
110
+ version: '0.17'
111
111
  description: Feature flippers with redis.
112
112
  email:
113
113
  - jamesgolick@gmail.com
@@ -121,13 +121,15 @@ files:
121
121
  - ".rubocop.yml"
122
122
  - ".travis.yml"
123
123
  - Gemfile
124
- - Gemfile.lock
125
124
  - LICENSE
126
125
  - README.md
127
126
  - Rakefile
128
127
  - lib/rollout.rb
128
+ - lib/rollout/feature.rb
129
+ - lib/rollout/logging.rb
129
130
  - lib/rollout/version.rb
130
131
  - rollout.gemspec
132
+ - spec/rollout/logging_spec.rb
131
133
  - spec/rollout_spec.rb
132
134
  - spec/spec_helper.rb
133
135
  homepage: https://github.com/FetLife/rollout
@@ -149,10 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  - !ruby/object:Gem::Version
150
152
  version: '0'
151
153
  requirements: []
152
- rubygems_version: 3.0.3
154
+ rubygems_version: 3.1.2
153
155
  signing_key:
154
156
  specification_version: 4
155
157
  summary: Feature flippers with redis.
156
- test_files:
157
- - spec/rollout_spec.rb
158
- - spec/spec_helper.rb
158
+ test_files: []
@@ -1,65 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- rollout (2.4.5)
5
- redis (~> 4.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- ast (2.4.0)
11
- diff-lcs (1.3)
12
- docile (1.3.2)
13
- fakeredis (0.7.0)
14
- redis (>= 3.2, < 5.0)
15
- jaro_winkler (1.5.3)
16
- json (2.2.0)
17
- parallel (1.17.0)
18
- parser (2.6.3.0)
19
- ast (~> 2.4.0)
20
- rainbow (3.0.0)
21
- redis (4.1.2)
22
- rspec (3.8.0)
23
- rspec-core (~> 3.8.0)
24
- rspec-expectations (~> 3.8.0)
25
- rspec-mocks (~> 3.8.0)
26
- rspec-core (3.8.2)
27
- rspec-support (~> 3.8.0)
28
- rspec-expectations (3.8.4)
29
- diff-lcs (>= 1.2.0, < 2.0)
30
- rspec-support (~> 3.8.0)
31
- rspec-mocks (3.8.1)
32
- diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.8.0)
34
- rspec-support (3.8.2)
35
- rspec_junit_formatter (0.4.1)
36
- rspec-core (>= 2, < 4, != 2.12.0)
37
- rubocop (0.72.0)
38
- jaro_winkler (~> 1.5.1)
39
- parallel (~> 1.10)
40
- parser (>= 2.6)
41
- rainbow (>= 2.2.2, < 4.0)
42
- ruby-progressbar (~> 1.7)
43
- unicode-display_width (>= 1.4.0, < 1.7)
44
- ruby-progressbar (1.10.1)
45
- simplecov (0.16.1)
46
- docile (~> 1.1)
47
- json (>= 1.8, < 3)
48
- simplecov-html (~> 0.10.0)
49
- simplecov-html (0.10.2)
50
- unicode-display_width (1.6.0)
51
-
52
- PLATFORMS
53
- ruby
54
-
55
- DEPENDENCIES
56
- bundler (~> 1.17)
57
- fakeredis
58
- rollout!
59
- rspec (~> 3.0)
60
- rspec_junit_formatter (~> 0.4)
61
- rubocop (~> 0.71)
62
- simplecov (~> 0.16)
63
-
64
- BUNDLED WITH
65
- 1.17.2