flipper 0.26.2 → 0.28.3

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +61 -0
  3. data/Gemfile +2 -3
  4. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  5. data/examples/api/basic.ru +3 -4
  6. data/examples/api/custom_memoized.ru +3 -4
  7. data/examples/api/memoized.ru +3 -4
  8. data/examples/dsl.rb +3 -3
  9. data/examples/enabled_for_actor.rb +4 -2
  10. data/examples/mirroring.rb +59 -0
  11. data/lib/flipper/adapter.rb +23 -7
  12. data/lib/flipper/adapters/http.rb +11 -3
  13. data/lib/flipper/adapters/instrumented.rb +25 -2
  14. data/lib/flipper/adapters/memoizable.rb +19 -2
  15. data/lib/flipper/adapters/memory.rb +40 -16
  16. data/lib/flipper/adapters/operation_logger.rb +16 -3
  17. data/lib/flipper/dsl.rb +5 -9
  18. data/lib/flipper/errors.rb +3 -3
  19. data/lib/flipper/export.rb +26 -0
  20. data/lib/flipper/exporter.rb +17 -0
  21. data/lib/flipper/exporters/json/export.rb +32 -0
  22. data/lib/flipper/exporters/json/v1.rb +33 -0
  23. data/lib/flipper/feature.rb +12 -10
  24. data/lib/flipper/feature_check_context.rb +8 -4
  25. data/lib/flipper/gate.rb +12 -11
  26. data/lib/flipper/gates/actor.rb +11 -8
  27. data/lib/flipper/gates/group.rb +4 -2
  28. data/lib/flipper/gates/percentage_of_actors.rb +4 -5
  29. data/lib/flipper/identifier.rb +2 -2
  30. data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
  31. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  32. data/lib/flipper/poller.rb +1 -1
  33. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  34. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  35. data/lib/flipper/typecast.rb +17 -0
  36. data/lib/flipper/types/actor.rb +13 -13
  37. data/lib/flipper/types/group.rb +4 -4
  38. data/lib/flipper/version.rb +1 -1
  39. data/lib/flipper.rb +5 -4
  40. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  41. data/spec/flipper/adapter_spec.rb +29 -2
  42. data/spec/flipper/adapters/http_spec.rb +25 -3
  43. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  44. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  45. data/spec/flipper/adapters/memory_spec.rb +11 -2
  46. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  47. data/spec/flipper/dsl_spec.rb +25 -8
  48. data/spec/flipper/export_spec.rb +13 -0
  49. data/spec/flipper/exporter_spec.rb +16 -0
  50. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  51. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  52. data/spec/flipper/feature_check_context_spec.rb +5 -5
  53. data/spec/flipper/feature_spec.rb +76 -32
  54. data/spec/flipper/gates/boolean_spec.rb +1 -1
  55. data/spec/flipper/gates/group_spec.rb +2 -3
  56. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  57. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  58. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
  59. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  60. data/spec/flipper/typecast_spec.rb +79 -0
  61. data/spec/flipper/types/actor_spec.rb +45 -45
  62. data/spec/flipper/types/group_spec.rb +2 -2
  63. data/spec/flipper_integration_spec.rb +62 -50
  64. data/spec/flipper_spec.rb +7 -1
  65. data/spec/support/skippable.rb +18 -0
  66. metadata +20 -2
@@ -9,7 +9,7 @@ RSpec.describe Flipper::Gates::Boolean do
9
9
  Flipper::FeatureCheckContext.new(
10
10
  feature_name: feature_name,
11
11
  values: Flipper::GateValues.new(boolean: bool),
12
- thing: Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
13
13
  )
14
14
  end
15
15
 
@@ -9,18 +9,17 @@ RSpec.describe Flipper::Gates::Group do
9
9
  Flipper::FeatureCheckContext.new(
10
10
  feature_name: feature_name,
11
11
  values: Flipper::GateValues.new(groups: set),
12
- thing: Flipper::Types::Actor.new(Flipper::Actor.new('5'))
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('5'))]
13
13
  )
14
14
  end
15
15
 
16
16
  describe '#open?' do
17
17
  context 'with a group in adapter, but not registered' do
18
18
  before do
19
- Flipper.register(:staff) { |_thing| true }
19
+ Flipper.register(:staff) { |actor| true }
20
20
  end
21
21
 
22
22
  it 'ignores group' do
23
- thing = Flipper::Actor.new('5')
24
23
  expect(subject.open?(context(Set[:newbs, :staff]))).to be(true)
25
24
  end
26
25
  end
@@ -5,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
5
5
  described_class.new
6
6
  end
7
7
 
8
- def context(percentage_of_actors_value, feature = feature_name, thing = nil)
8
+ def context(percentage_of_actors_value, feature = feature_name, actors = nil)
9
9
  Flipper::FeatureCheckContext.new(
10
10
  feature_name: feature,
11
11
  values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
12
- thing: Flipper::Types::Actor.new(thing || Flipper::Actor.new(1))
12
+ actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
13
13
  )
14
14
  end
15
15
 
@@ -20,7 +20,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
20
20
  let(:number_of_actors) { 10_000 }
21
21
 
22
22
  let(:actors) do
23
- (1..number_of_actors).map { |n| Flipper::Actor.new(n) }
23
+ (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
24
24
  end
25
25
 
26
26
  let(:feature_one_enabled_actors) do
@@ -48,13 +48,69 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
48
48
  end
49
49
  end
50
50
 
51
+ context "with an array of actors" do
52
+ let(:percentage) { 0.05 }
53
+ let(:percentage_as_integer) { percentage * 100 }
54
+ let(:number_of_actors) { 3_000 }
55
+
56
+ let(:user_actors) do
57
+ (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("User;#{n}")) }
58
+ end
59
+
60
+ let(:team_actors) do
61
+ (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Team;#{n}")) }
62
+ end
63
+
64
+ let(:org_actors) do
65
+ (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Org;#{n}")) }
66
+ end
67
+
68
+ let(:actors) { user_actors + team_actors + org_actors }
69
+
70
+ let(:feature_one_enabled_actors) do
71
+ actors.each_slice(3).select do |group|
72
+ context = context(percentage_as_integer, :name_one, group)
73
+ subject.open?(context)
74
+ end.flatten
75
+ end
76
+
77
+ let(:feature_two_enabled_actors) do
78
+ actors.each_slice(3).select do |group|
79
+ context = context(percentage_as_integer, :name_two, group)
80
+ subject.open?(context)
81
+ end.flatten
82
+ end
83
+
84
+ it 'does not enable both features for same set of actors' do
85
+ expect(feature_one_enabled_actors).not_to eq(feature_two_enabled_actors)
86
+ end
87
+
88
+ it 'enables feature for accurate number of actors for each feature' do
89
+ margin_of_error = 0.02 * actors.size # 2 percent margin of error
90
+ expected_enabled_size = actors.size * percentage
91
+
92
+ [
93
+ feature_one_enabled_actors.size,
94
+ feature_two_enabled_actors.size,
95
+ ].each do |size|
96
+ expect(size).to be_within(margin_of_error).of(expected_enabled_size)
97
+ end
98
+ end
99
+
100
+ it "is consistent regardless of order of actors" do
101
+ actors = user_actors.first(10)
102
+ results = 100.times.map { |n| subject.open?(context(75, :some_feature, actors.shuffle)) }
103
+ expect(results.uniq).to eq([true])
104
+ end
105
+ end
106
+
51
107
  context 'for fractional percentage' do
52
108
  let(:decimal) { 0.001 }
53
109
  let(:percentage) { decimal * 100 }
54
110
  let(:number_of_actors) { 10_000 }
55
111
 
56
112
  let(:actors) do
57
- (1..number_of_actors).map { |n| Flipper::Actor.new(n) }
113
+ (1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
58
114
  end
59
115
 
60
116
  subject { described_class.new }
@@ -64,7 +120,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
64
120
  expected_open_count = number_of_actors * decimal
65
121
 
66
122
  open_count = actors.select do |actor|
67
- context = context(percentage, :feature, actor)
123
+ context = context(percentage, :feature, [actor])
68
124
  subject.open?(context)
69
125
  end.size
70
126
 
@@ -5,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfTime do
5
5
  described_class.new
6
6
  end
7
7
 
8
- def context(percentage_of_time_value, feature = feature_name, thing = nil)
8
+ def context(percentage_of_time_value, feature = feature_name, actors = nil)
9
9
  Flipper::FeatureCheckContext.new(
10
10
  feature_name: feature,
11
11
  values: Flipper::GateValues.new(percentage_of_time: percentage_of_time_value),
12
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
13
13
  )
14
14
  end
15
15
 
@@ -2,6 +2,12 @@ require 'logger'
2
2
  require 'flipper/adapters/instrumented'
3
3
  require 'flipper/instrumentation/log_subscriber'
4
4
 
5
+ begin
6
+ require 'active_support/isolated_execution_state'
7
+ rescue LoadError
8
+ # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
+ end
10
+
5
11
  RSpec.describe Flipper::Instrumentation::LogSubscriber do
6
12
  let(:adapter) do
7
13
  memory = Flipper::Adapters::Memory.new
@@ -12,8 +18,8 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
12
18
  end
13
19
 
14
20
  before do
15
- Flipper.register(:admins) do |thing|
16
- thing.respond_to?(:admin?) && thing.admin?
21
+ Flipper.register(:admins) do |actor|
22
+ actor.respond_to?(:admin?) && actor.admin?
17
23
  end
18
24
 
19
25
  @io = StringIO.new
@@ -26,6 +32,10 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
26
32
  described_class.logger = nil
27
33
  end
28
34
 
35
+ after(:all) do
36
+ ActiveSupport::Notifications.unsubscribe("flipper")
37
+ end
38
+
29
39
  let(:log) { @io.string }
30
40
 
31
41
  context 'feature enabled checks' do
@@ -36,7 +46,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
36
46
 
37
47
  it 'logs feature calls with result after operation' do
38
48
  feature_line = find_line('Flipper feature(search) enabled? false')
39
- expect(feature_line).to include('[ thing=nil ]')
49
+ expect(feature_line).to include('[ actors=nil ]')
40
50
  end
41
51
 
42
52
  it 'logs adapter calls' do
@@ -46,7 +56,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
46
56
  end
47
57
  end
48
58
 
49
- context 'feature enabled checks with a thing' do
59
+ context 'feature enabled checks with an actor' do
50
60
  let(:user) { Flipper::Types::Actor.new(Flipper::Actor.new('1')) }
51
61
 
52
62
  before do
@@ -54,7 +64,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
54
64
  flipper[:search].enabled?(user)
55
65
  end
56
66
 
57
- it 'logs thing for feature' do
67
+ it 'logs actors for feature' do
58
68
  feature_line = find_line('Flipper feature(search) enabled?')
59
69
  expect(feature_line).to include(user.inspect)
60
70
  end
@@ -2,6 +2,12 @@ require 'flipper/adapters/instrumented'
2
2
  require 'flipper/instrumentation/statsd'
3
3
  require 'statsd'
4
4
 
5
+ begin
6
+ require 'active_support/isolated_execution_state'
7
+ rescue LoadError
8
+ # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
+ end
10
+
5
11
  RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
6
12
  let(:statsd_client) { Statsd.new }
7
13
  let(:socket) { FakeUDPSocket.new }
@@ -25,6 +31,10 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
25
31
  Thread.current[:statsd_socket] = nil
26
32
  end
27
33
 
34
+ after(:all) do
35
+ ActiveSupport::Notifications.unsubscribe("flipper")
36
+ end
37
+
28
38
  def assert_timer(metric)
29
39
  regex = /#{Regexp.escape metric}\:\d+\|ms/
30
40
  result = socket.buffer.detect { |op| op.first =~ regex }
@@ -114,4 +114,83 @@ RSpec.describe Flipper::Typecast do
114
114
  described_class.to_set('asdf')
115
115
  end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
116
116
  end
117
+
118
+ describe "#features_hash" do
119
+ it "returns new hash" do
120
+ hash = {
121
+ "search" => {
122
+ boolean: nil,
123
+ }
124
+ }
125
+ result = described_class.features_hash(hash)
126
+ expect(result).not_to be(hash)
127
+ expect(result["search"]).not_to be(hash["search"])
128
+ end
129
+
130
+ it "converts gate value arrays to sets" do
131
+ hash = {
132
+ "search" => {
133
+ boolean: nil,
134
+ groups: ['a', 'b'],
135
+ actors: ['User;1'],
136
+ percentage_of_actors: nil,
137
+ percentage_of_time: nil,
138
+ },
139
+ }
140
+ result = described_class.features_hash(hash)
141
+ expect(result).to eq({
142
+ "search" => {
143
+ boolean: nil,
144
+ groups: Set['a', 'b'],
145
+ actors: Set['User;1'],
146
+ percentage_of_actors: nil,
147
+ percentage_of_time: nil,
148
+ },
149
+ })
150
+ end
151
+
152
+ it "converts gate value boolean and integers to strings" do
153
+ hash = {
154
+ "search" => {
155
+ boolean: true,
156
+ groups: Set.new,
157
+ actors: Set.new,
158
+ percentage_of_actors: 10,
159
+ percentage_of_time: 15,
160
+ },
161
+ }
162
+ result = described_class.features_hash(hash)
163
+ expect(result).to eq({
164
+ "search" => {
165
+ boolean: "true",
166
+ groups: Set.new,
167
+ actors: Set.new,
168
+ percentage_of_actors: "10",
169
+ percentage_of_time: "15",
170
+ },
171
+ })
172
+ end
173
+
174
+ it "converts string gate keys to symbols" do
175
+ hash = {
176
+ "search" => {
177
+ "boolean" => nil,
178
+ "groups" => Set.new,
179
+ "actors" => Set.new,
180
+ "percentage_of_actors" => nil,
181
+ "percentage_of_time" => nil,
182
+ },
183
+ }
184
+ result = described_class.features_hash(hash)
185
+ expect(result).to eq({
186
+ "search" => {
187
+ boolean: nil,
188
+ groups: Set.new,
189
+ actors: Set.new,
190
+ percentage_of_actors: nil,
191
+ percentage_of_time: nil,
192
+ },
193
+ })
194
+ end
195
+ end
117
196
  end
@@ -2,11 +2,11 @@ require 'flipper/types/actor'
2
2
 
3
3
  RSpec.describe Flipper::Types::Actor do
4
4
  subject do
5
- thing = thing_class.new('2')
6
- described_class.new(thing)
5
+ actor = actor_class.new('2')
6
+ described_class.new(actor)
7
7
  end
8
8
 
9
- let(:thing_class) do
9
+ let(:actor_class) do
10
10
  Class.new do
11
11
  attr_reader :flipper_id
12
12
 
@@ -22,14 +22,14 @@ RSpec.describe Flipper::Types::Actor do
22
22
 
23
23
  describe '.wrappable?' do
24
24
  it 'returns true if actor' do
25
- thing = thing_class.new('1')
26
- actor = described_class.new(thing)
27
- expect(described_class.wrappable?(actor)).to eq(true)
25
+ actor = actor_class.new('1')
26
+ actor_type_instance = described_class.new(actor)
27
+ expect(described_class.wrappable?(actor_type_instance)).to eq(true)
28
28
  end
29
29
 
30
30
  it 'returns true if responds to flipper_id' do
31
- thing = thing_class.new(10)
32
- expect(described_class.wrappable?(thing)).to eq(true)
31
+ actor = actor_class.new(10)
32
+ expect(described_class.wrappable?(actor)).to eq(true)
33
33
  end
34
34
 
35
35
  it 'returns false if nil' do
@@ -38,27 +38,27 @@ RSpec.describe Flipper::Types::Actor do
38
38
  end
39
39
 
40
40
  describe '.wrap' do
41
- context 'for actor' do
42
- it 'returns actor' do
43
- actor = described_class.wrap(subject)
44
- expect(actor).to be_instance_of(described_class)
45
- expect(actor).to be(subject)
41
+ context 'for actor type instance' do
42
+ it 'returns actor type instance' do
43
+ actor_type_instance = described_class.wrap(subject)
44
+ expect(actor_type_instance).to be_instance_of(described_class)
45
+ expect(actor_type_instance).to be(subject)
46
46
  end
47
47
  end
48
48
 
49
- context 'for other thing' do
50
- it 'returns actor' do
51
- thing = thing_class.new('1')
52
- actor = described_class.wrap(thing)
53
- expect(actor).to be_instance_of(described_class)
49
+ context 'for other object' do
50
+ it 'returns actor type instance' do
51
+ actor = actor_class.new('1')
52
+ actor_type_instance = described_class.wrap(actor)
53
+ expect(actor_type_instance).to be_instance_of(described_class)
54
54
  end
55
55
  end
56
56
  end
57
57
 
58
- it 'initializes with thing that responds to id' do
59
- thing = thing_class.new('1')
60
- actor = described_class.new(thing)
61
- expect(actor.value).to eq('1')
58
+ it 'initializes with object that responds to flipper_id' do
59
+ actor = actor_class.new('1')
60
+ actor_type_instance = described_class.new(actor)
61
+ expect(actor_type_instance.value).to eq('1')
62
62
  end
63
63
 
64
64
  it 'raises error when initialized with nil' do
@@ -68,48 +68,48 @@ RSpec.describe Flipper::Types::Actor do
68
68
  end
69
69
 
70
70
  it 'raises error when initalized with non-wrappable object' do
71
- unwrappable_thing = Struct.new(:id).new(1)
71
+ unwrappable_object = Struct.new(:id).new(1)
72
72
  expect do
73
- described_class.new(unwrappable_thing)
73
+ described_class.new(unwrappable_object)
74
74
  end.to raise_error(ArgumentError,
75
- "#{unwrappable_thing.inspect} must respond to flipper_id, but does not")
75
+ "#{unwrappable_object.inspect} must respond to flipper_id, but does not")
76
76
  end
77
77
 
78
78
  it 'converts id to string' do
79
- thing = thing_class.new(2)
80
- actor = described_class.new(thing)
79
+ actor = actor_class.new(2)
80
+ actor = described_class.new(actor)
81
81
  expect(actor.value).to eq('2')
82
82
  end
83
83
 
84
- it 'proxies everything to thing' do
85
- thing = thing_class.new(10)
86
- actor = described_class.new(thing)
84
+ it 'proxies everything to actor' do
85
+ actor = actor_class.new(10)
86
+ actor = described_class.new(actor)
87
87
  expect(actor.admin?).to eq(true)
88
88
  end
89
89
 
90
- it 'exposes thing' do
91
- thing = thing_class.new(10)
92
- actor = described_class.new(thing)
93
- expect(actor.thing).to be(thing)
90
+ it 'exposes actor' do
91
+ actor = actor_class.new(10)
92
+ actor_type_instance = described_class.new(actor)
93
+ expect(actor_type_instance.actor).to be(actor)
94
94
  end
95
95
 
96
96
  describe '#respond_to?' do
97
97
  it 'returns true if responds to method' do
98
- thing = thing_class.new('1')
99
- actor = described_class.new(thing)
100
- expect(actor.respond_to?(:value)).to eq(true)
98
+ actor = actor_class.new('1')
99
+ actor_type_instance = described_class.new(actor)
100
+ expect(actor_type_instance.respond_to?(:value)).to eq(true)
101
101
  end
102
102
 
103
- it 'returns true if thing responds to method' do
104
- thing = thing_class.new(10)
105
- actor = described_class.new(thing)
106
- expect(actor.respond_to?(:admin?)).to eq(true)
103
+ it 'returns true if actor responds to method' do
104
+ actor = actor_class.new(10)
105
+ actor_type_instance = described_class.new(actor)
106
+ expect(actor_type_instance.respond_to?(:admin?)).to eq(true)
107
107
  end
108
108
 
109
- it 'returns false if does not respond to method and thing does not respond to method' do
110
- thing = thing_class.new(10)
111
- actor = described_class.new(thing)
112
- expect(actor.respond_to?(:frankenstein)).to eq(false)
109
+ it 'returns false if does not respond to method and actor does not respond to method' do
110
+ actor = actor_class.new(10)
111
+ actor_type_instance = described_class.new(actor)
112
+ expect(actor_type_instance.respond_to?(:frankenstein)).to eq(false)
113
113
  end
114
114
  end
115
115
  end
@@ -90,7 +90,7 @@ RSpec.describe Flipper::Types::Group do
90
90
  context = Flipper::FeatureCheckContext.new(
91
91
  feature_name: :my_feature,
92
92
  values: Flipper::GateValues.new({}),
93
- thing: Flipper::Types::Actor.new(Flipper::Actor.new(1))
93
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
94
94
  )
95
95
  group = Flipper.register(:group_with_context) { |actor| actor }
96
96
  yielded_actor = group.match?(admin_actor, context)
@@ -101,7 +101,7 @@ RSpec.describe Flipper::Types::Group do
101
101
  context = Flipper::FeatureCheckContext.new(
102
102
  feature_name: :my_feature,
103
103
  values: Flipper::GateValues.new({}),
104
- thing: Flipper::Types::Actor.new(Flipper::Actor.new(1))
104
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
105
105
  )
106
106
  group = Flipper.register(:group_with_context) { |actor, context| [actor, context] }
107
107
  yielded_actor, yielded_context = group.match?(admin_actor, context)