rollout 2.4.5 → 2.5.0

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: 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