flipper 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +7 -3
  4. data/.github/workflows/examples.yml +27 -5
  5. data/Changelog.md +42 -0
  6. data/Gemfile +4 -4
  7. data/README.md +13 -11
  8. data/benchmark/typecast_ips.rb +8 -0
  9. data/docs/images/flipper_cloud.png +0 -0
  10. data/examples/cloud/backoff_policy.rb +13 -0
  11. data/examples/cloud/cloud_setup.rb +16 -0
  12. data/examples/cloud/forked.rb +7 -2
  13. data/examples/cloud/threaded.rb +15 -18
  14. data/examples/expressions.rb +213 -0
  15. data/examples/strict.rb +18 -0
  16. data/flipper.gemspec +1 -2
  17. data/lib/flipper/actor.rb +6 -3
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapter_builder.rb +44 -0
  20. data/lib/flipper/adapters/dual_write.rb +1 -3
  21. data/lib/flipper/adapters/failover.rb +0 -4
  22. data/lib/flipper/adapters/failsafe.rb +0 -4
  23. data/lib/flipper/adapters/http/client.rb +26 -7
  24. data/lib/flipper/adapters/http/error.rb +1 -1
  25. data/lib/flipper/adapters/http.rb +18 -13
  26. data/lib/flipper/adapters/instrumented.rb +0 -4
  27. data/lib/flipper/adapters/memoizable.rb +14 -19
  28. data/lib/flipper/adapters/memory.rb +4 -6
  29. data/lib/flipper/adapters/operation_logger.rb +0 -4
  30. data/lib/flipper/adapters/poll.rb +1 -3
  31. data/lib/flipper/adapters/pstore.rb +17 -11
  32. data/lib/flipper/adapters/read_only.rb +4 -4
  33. data/lib/flipper/adapters/strict.rb +47 -0
  34. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  35. data/lib/flipper/adapters/sync.rb +0 -4
  36. data/lib/flipper/cloud/configuration.rb +121 -52
  37. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  38. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  39. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  40. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  41. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  42. data/lib/flipper/cloud/telemetry.rb +183 -0
  43. data/lib/flipper/configuration.rb +25 -4
  44. data/lib/flipper/dsl.rb +51 -0
  45. data/lib/flipper/engine.rb +28 -3
  46. data/lib/flipper/exporters/json/export.rb +1 -1
  47. data/lib/flipper/exporters/json/v1.rb +1 -1
  48. data/lib/flipper/expression/builder.rb +73 -0
  49. data/lib/flipper/expression/constant.rb +25 -0
  50. data/lib/flipper/expression.rb +71 -0
  51. data/lib/flipper/expressions/all.rb +11 -0
  52. data/lib/flipper/expressions/any.rb +9 -0
  53. data/lib/flipper/expressions/boolean.rb +9 -0
  54. data/lib/flipper/expressions/comparable.rb +13 -0
  55. data/lib/flipper/expressions/duration.rb +28 -0
  56. data/lib/flipper/expressions/equal.rb +9 -0
  57. data/lib/flipper/expressions/greater_than.rb +9 -0
  58. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  59. data/lib/flipper/expressions/less_than.rb +9 -0
  60. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  61. data/lib/flipper/expressions/not_equal.rb +9 -0
  62. data/lib/flipper/expressions/now.rb +9 -0
  63. data/lib/flipper/expressions/number.rb +9 -0
  64. data/lib/flipper/expressions/percentage.rb +9 -0
  65. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  66. data/lib/flipper/expressions/property.rb +9 -0
  67. data/lib/flipper/expressions/random.rb +9 -0
  68. data/lib/flipper/expressions/string.rb +9 -0
  69. data/lib/flipper/expressions/time.rb +9 -0
  70. data/lib/flipper/feature.rb +55 -0
  71. data/lib/flipper/gate.rb +1 -0
  72. data/lib/flipper/gate_values.rb +5 -2
  73. data/lib/flipper/gates/expression.rb +75 -0
  74. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  75. data/lib/flipper/middleware/memoizer.rb +29 -13
  76. data/lib/flipper/poller.rb +1 -1
  77. data/lib/flipper/serializers/gzip.rb +24 -0
  78. data/lib/flipper/serializers/json.rb +19 -0
  79. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  80. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  81. data/lib/flipper/typecast.rb +34 -6
  82. data/lib/flipper/types/percentage.rb +1 -1
  83. data/lib/flipper/version.rb +1 -1
  84. data/lib/flipper.rb +38 -1
  85. data/spec/flipper/adapter_builder_spec.rb +73 -0
  86. data/spec/flipper/adapter_spec.rb +1 -0
  87. data/spec/flipper/adapters/http_spec.rb +39 -5
  88. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  89. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  90. data/spec/flipper/adapters/strict_spec.rb +62 -0
  91. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  92. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  93. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  94. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  95. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  96. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  97. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  98. data/spec/flipper/cloud_spec.rb +12 -12
  99. data/spec/flipper/configuration_spec.rb +17 -0
  100. data/spec/flipper/dsl_spec.rb +39 -0
  101. data/spec/flipper/engine_spec.rb +108 -7
  102. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  103. data/spec/flipper/expression/builder_spec.rb +248 -0
  104. data/spec/flipper/expression_spec.rb +188 -0
  105. data/spec/flipper/expressions/all_spec.rb +15 -0
  106. data/spec/flipper/expressions/any_spec.rb +15 -0
  107. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  108. data/spec/flipper/expressions/duration_spec.rb +43 -0
  109. data/spec/flipper/expressions/equal_spec.rb +24 -0
  110. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  111. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  112. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  114. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  115. data/spec/flipper/expressions/now_spec.rb +11 -0
  116. data/spec/flipper/expressions/number_spec.rb +21 -0
  117. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  118. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  119. data/spec/flipper/expressions/property_spec.rb +13 -0
  120. data/spec/flipper/expressions/random_spec.rb +9 -0
  121. data/spec/flipper/expressions/string_spec.rb +11 -0
  122. data/spec/flipper/expressions/time_spec.rb +13 -0
  123. data/spec/flipper/feature_spec.rb +360 -1
  124. data/spec/flipper/gate_values_spec.rb +2 -2
  125. data/spec/flipper/gates/expression_spec.rb +108 -0
  126. data/spec/flipper/identifier_spec.rb +4 -5
  127. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  128. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  129. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  130. data/spec/flipper/serializers/json_spec.rb +13 -0
  131. data/spec/flipper/typecast_spec.rb +43 -7
  132. data/spec/flipper/types/actor_spec.rb +18 -1
  133. data/spec/flipper_integration_spec.rb +102 -4
  134. data/spec/flipper_spec.rb +89 -1
  135. data/spec/spec_helper.rb +5 -0
  136. data/spec/support/actor_names.yml +1 -0
  137. data/spec/support/fake_backoff_policy.rb +15 -0
  138. data/spec/support/spec_helpers.rb +11 -3
  139. metadata +104 -18
  140. data/lib/flipper/cloud/instrumenter.rb +0 -48
data/lib/flipper.rb CHANGED
@@ -57,15 +57,49 @@ module Flipper
57
57
  # interface of Flipper::DSL.
58
58
  def_delegators :instance,
59
59
  :enabled?, :enable, :disable,
60
+ :enable_expression, :disable_expression,
61
+ :expression, :add_expression, :remove_expression,
60
62
  :enable_actor, :disable_actor,
61
63
  :enable_group, :disable_group,
62
64
  :enable_percentage_of_actors, :disable_percentage_of_actors,
63
65
  :enable_percentage_of_time, :disable_percentage_of_time,
64
66
  :features, :feature, :[], :preload, :preload_all,
65
67
  :adapter, :add, :exist?, :remove, :import, :export,
66
- :memoize=, :memoizing?,
68
+ :memoize=, :memoizing?, :read_only?,
67
69
  :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
68
70
 
71
+ def any(*args)
72
+ Expression.build({ Any: args.flatten })
73
+ end
74
+
75
+ def all(*args)
76
+ Expression.build({ All: args.flatten })
77
+ end
78
+
79
+ def constant(value)
80
+ Expression.build(value)
81
+ end
82
+
83
+ def property(name)
84
+ Expression.build({ Property: name })
85
+ end
86
+
87
+ def string(value)
88
+ Expression.build({ String: value })
89
+ end
90
+
91
+ def number(value)
92
+ Expression.build({ Number: value })
93
+ end
94
+
95
+ def boolean(value)
96
+ Expression.build({ Boolean: value })
97
+ end
98
+
99
+ def random(max)
100
+ Expression.build({ Random: max })
101
+ end
102
+
69
103
  # Public: Use this to register a group by name.
70
104
  #
71
105
  # name - The Symbol name of the group.
@@ -142,7 +176,9 @@ require 'flipper/actor'
142
176
  require 'flipper/adapter'
143
177
  require 'flipper/adapters/memoizable'
144
178
  require 'flipper/adapters/memory'
179
+ require 'flipper/adapters/strict'
145
180
  require 'flipper/adapters/instrumented'
181
+ require 'flipper/adapter_builder'
146
182
  require 'flipper/configuration'
147
183
  require 'flipper/dsl'
148
184
  require 'flipper/errors'
@@ -155,6 +191,7 @@ require 'flipper/middleware/memoizer'
155
191
  require 'flipper/middleware/setup_env'
156
192
  require 'flipper/poller'
157
193
  require 'flipper/registry'
194
+ require 'flipper/expression'
158
195
  require 'flipper/type'
159
196
  require 'flipper/types/actor'
160
197
  require 'flipper/types/boolean'
@@ -0,0 +1,73 @@
1
+ RSpec.describe Flipper::AdapterBuilder do
2
+ describe "#initialize" do
3
+ it "instance_eval's block with no arg" do
4
+ called = false
5
+ self_in_block = nil
6
+
7
+ described_class.new do
8
+ called = true
9
+ self_in_block = self
10
+ end
11
+
12
+ expect(self_in_block).to be_instance_of(described_class)
13
+ expect(called).to be(true)
14
+ end
15
+
16
+ it "evals block with arg" do
17
+ called = false
18
+ self_outside_block = self
19
+ self_in_block = nil
20
+
21
+ described_class.new do |arg|
22
+ called = true
23
+ self_in_block = self
24
+ expect(arg).to be_instance_of(described_class)
25
+ end
26
+
27
+ expect(self_in_block).to be(self_outside_block)
28
+ expect(called).to be(true)
29
+ end
30
+ end
31
+
32
+ describe "#use" do
33
+ it "wraps the store adapter with the given adapter" do
34
+ subject.use(Flipper::Adapters::Memoizable)
35
+ subject.use(Flipper::Adapters::Strict, :warn)
36
+
37
+ memoizable_adapter = subject.to_adapter
38
+ strict_adapter = memoizable_adapter.adapter
39
+ memory_adapter = strict_adapter.adapter
40
+
41
+
42
+ expect(memoizable_adapter).to be_instance_of(Flipper::Adapters::Memoizable)
43
+ expect(strict_adapter).to be_instance_of(Flipper::Adapters::Strict)
44
+ expect(strict_adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
45
+ expect(memory_adapter).to be_instance_of(Flipper::Adapters::Memory)
46
+ end
47
+
48
+ it "passes block to adapter initializer" do
49
+ expected_block = lambda {}
50
+ adapter_class = double('adapter class')
51
+
52
+ subject.use(adapter_class, &expected_block)
53
+
54
+ expect(adapter_class).to receive(:new) { |&block| expect(block).to be(expected_block) }.and_return(:adapter)
55
+ expect(subject.to_adapter).to be(:adapter)
56
+ end
57
+ end
58
+
59
+ describe "#store" do
60
+ it "defaults to memory adapter" do
61
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
62
+ end
63
+
64
+ it "only saves one store" do
65
+ require "flipper/adapters/pstore"
66
+ subject.store(Flipper::Adapters::PStore)
67
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::PStore)
68
+
69
+ subject.store(Flipper::Adapters::Memory)
70
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
71
+ end
72
+ end
73
+ end
@@ -6,6 +6,7 @@ RSpec.describe Flipper::Adapter do
6
6
  boolean: nil,
7
7
  groups: Set.new,
8
8
  actors: Set.new,
9
+ expression: nil,
9
10
  percentage_of_actors: nil,
10
11
  percentage_of_time: nil,
11
12
  }
@@ -84,16 +84,50 @@ RSpec.describe Flipper::Adapters::Http do
84
84
  }
85
85
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
86
86
  .with(headers: headers)
87
- .to_return(status: 404, body: "", headers: {})
87
+ .to_return(status: 404)
88
88
 
89
89
  adapter = described_class.new(url: 'http://app.com/flipper')
90
90
  adapter.get(flipper[:feature_panel])
91
91
  end
92
92
 
93
+ it "sends framework versions" do
94
+ stub_const("Rails", double(version: "7.1.0"))
95
+ stub_const("Sinatra::VERSION", "3.1.0")
96
+ stub_const("Hanami::VERSION", "0.7.2")
97
+
98
+ headers = {
99
+ "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0", "hanami=0.7.2"]
100
+ }
101
+
102
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
103
+ .with(headers: headers)
104
+ .to_return(status: 404)
105
+
106
+ adapter = described_class.new(url: 'http://app.com/flipper')
107
+ adapter.get(flipper[:feature_panel])
108
+ end
109
+
110
+ it "does not send undefined framework versions" do
111
+ stub_const("Rails", double(version: "7.1.0"))
112
+ stub_const("Sinatra::VERSION", "3.1.0")
113
+
114
+ headers = {
115
+ "Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0"]
116
+ }
117
+
118
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
119
+ .with(headers: headers)
120
+ .to_return(status: 404)
121
+
122
+ adapter = described_class.new(url: 'http://app.com/flipper')
123
+ adapter.get(flipper[:feature_panel])
124
+ end
125
+
126
+
93
127
  describe "#get" do
94
128
  it "raises error when not successful response" do
95
129
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
96
- .to_return(status: 503, body: "", headers: {})
130
+ .to_return(status: 503)
97
131
 
98
132
  adapter = described_class.new(url: 'http://app.com/flipper')
99
133
  expect {
@@ -105,7 +139,7 @@ RSpec.describe Flipper::Adapters::Http do
105
139
  describe "#get_multi" do
106
140
  it "raises error when not successful response" do
107
141
  stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
108
- .to_return(status: 503, body: "", headers: {})
142
+ .to_return(status: 503)
109
143
 
110
144
  adapter = described_class.new(url: 'http://app.com/flipper')
111
145
  expect {
@@ -117,7 +151,7 @@ RSpec.describe Flipper::Adapters::Http do
117
151
  describe "#get_all" do
118
152
  it "raises error when not successful response" do
119
153
  stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
120
- .to_return(status: 503, body: "", headers: {})
154
+ .to_return(status: 503)
121
155
 
122
156
  adapter = described_class.new(url: 'http://app.com/flipper')
123
157
  expect {
@@ -129,7 +163,7 @@ RSpec.describe Flipper::Adapters::Http do
129
163
  describe "#features" do
130
164
  it "raises error when not successful response" do
131
165
  stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
132
- .to_return(status: 503, body: "", headers: {})
166
+ .to_return(status: 503)
133
167
 
134
168
  adapter = described_class.new(url: 'http://app.com/flipper')
135
169
  expect {
@@ -2,7 +2,7 @@ require 'flipper/adapters/memoizable'
2
2
  require 'flipper/adapters/operation_logger'
3
3
 
4
4
  RSpec.describe Flipper::Adapters::Memoizable do
5
- let(:features_key) { described_class::FeaturesKey }
5
+ let(:features_key) { :flipper_features }
6
6
  let(:adapter) { Flipper::Adapters::Memory.new }
7
7
  let(:flipper) { Flipper.new(adapter) }
8
8
  let(:cache) { {} }
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
54
54
  it 'memoizes feature' do
55
55
  feature = flipper[:stats]
56
56
  result = subject.get(feature)
57
- expect(cache[described_class.key_for(feature.key)]).to be(result)
57
+ expect(cache["feature/#{feature.key}"]).to be(result)
58
58
  end
59
59
  end
60
60
 
@@ -83,8 +83,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
83
83
  features = names.map { |name| flipper[name] }
84
84
  results = subject.get_multi(features)
85
85
  features.each do |feature|
86
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
87
- expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key])
86
+ expect(cache["feature/#{feature.key}"]).not_to be(nil)
87
+ expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
88
88
  end
89
89
  end
90
90
  end
@@ -115,10 +115,10 @@ RSpec.describe Flipper::Adapters::Memoizable do
115
115
  features = names.map { |name| flipper[name].tap(&:enable) }
116
116
  results = subject.get_all
117
117
  features.each do |feature|
118
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
119
- expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key])
118
+ expect(cache["feature/#{feature.key}"]).not_to be(nil)
119
+ expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
120
120
  end
121
- expect(cache[subject.class::FeaturesKey]).to eq(names.map(&:to_s).to_set)
121
+ expect(cache[:flipper_features]).to eq(names.map(&:to_s).to_set)
122
122
  end
123
123
 
124
124
  it 'only calls get_all once for memoized adapter' do
@@ -188,9 +188,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
188
188
  it 'unmemoizes feature' do
189
189
  feature = flipper[:stats]
190
190
  gate = feature.gate(:boolean)
191
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
191
+ cache["feature/#{feature.key}"] = { some: 'thing' }
192
192
  subject.enable(feature, gate, Flipper::Types::Boolean.new)
193
- expect(cache[described_class.key_for(feature.key)]).to be_nil
193
+ expect(cache["feature/#{feature.key}"]).to be_nil
194
194
  end
195
195
  end
196
196
 
@@ -218,9 +218,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
218
218
  it 'unmemoizes feature' do
219
219
  feature = flipper[:stats]
220
220
  gate = feature.gate(:boolean)
221
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
221
+ cache["feature/#{feature.key}"] = { some: 'thing' }
222
222
  subject.disable(feature, gate, Flipper::Types::Boolean.new)
223
- expect(cache[described_class.key_for(feature.key)]).to be_nil
223
+ expect(cache["feature/#{feature.key}"]).to be_nil
224
224
  end
225
225
  end
226
226
 
@@ -332,9 +332,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
332
332
 
333
333
  it 'unmemoizes the feature' do
334
334
  feature = flipper[:stats]
335
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
335
+ cache["feature/#{feature.key}"] = { some: 'thing' }
336
336
  subject.remove(feature)
337
- expect(cache[described_class.key_for(feature.key)]).to be_nil
337
+ expect(cache["feature/#{feature.key}"]).to be_nil
338
338
  end
339
339
  end
340
340
 
@@ -357,9 +357,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
357
357
 
358
358
  it 'unmemoizes feature' do
359
359
  feature = flipper[:stats]
360
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
360
+ cache["feature/#{feature.key}"] = { some: 'thing' }
361
361
  subject.clear(feature)
362
- expect(cache[described_class.key_for(feature.key)]).to be_nil
362
+ expect(cache["feature/#{feature.key}"]).to be_nil
363
363
  end
364
364
  end
365
365
 
@@ -5,11 +5,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
5
5
  let(:flipper) { Flipper.new(subject) }
6
6
  let(:feature) { flipper[:stats] }
7
7
 
8
- let(:boolean_gate) { feature.gate(:boolean) }
9
- let(:group_gate) { feature.gate(:group) }
10
- let(:actor_gate) { feature.gate(:actor) }
11
- let(:actors_gate) { feature.gate(:percentage_of_actors) }
12
- let(:time_gate) { feature.gate(:percentage_of_time) }
8
+ let(:boolean_gate) { feature.gate(:boolean) }
9
+ let(:group_gate) { feature.gate(:group) }
10
+ let(:actor_gate) { feature.gate(:actor) }
11
+ let(:expression_gate) { feature.gate(:expression) }
12
+ let(:actors_gate) { feature.gate(:percentage_of_actors) }
13
+ let(:time_gate) { feature.gate(:percentage_of_time) }
13
14
 
14
15
  subject { described_class.new(adapter) }
15
16
 
@@ -41,18 +42,28 @@ RSpec.describe Flipper::Adapters::ReadOnly do
41
42
  end
42
43
 
43
44
  it 'can get feature' do
45
+ expression = Flipper.property(:plan).eq("basic")
44
46
  actor22 = Flipper::Actor.new('22')
45
47
  adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
46
48
  adapter.enable(feature, group_gate, flipper.group(:admins))
47
49
  adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
48
50
  adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
49
51
  adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
50
-
51
- expect(subject.get(feature)).to eq(boolean: 'true',
52
- groups: Set['admins'],
53
- actors: Set['22'],
54
- percentage_of_actors: '25',
55
- percentage_of_time: '45')
52
+ adapter.enable(feature, expression_gate, expression)
53
+
54
+ expect(subject.get(feature)).to eq({
55
+ boolean: 'true',
56
+ groups: Set['admins'],
57
+ actors: Set['22'],
58
+ expression: {
59
+ "Equal" => [
60
+ {"Property" => ["plan"]},
61
+ "basic",
62
+ ]
63
+ },
64
+ percentage_of_actors: '25',
65
+ percentage_of_time: '45',
66
+ })
56
67
  end
57
68
 
58
69
  it 'can get features' do
@@ -61,6 +72,10 @@ RSpec.describe Flipper::Adapters::ReadOnly do
61
72
  expect(subject.features).to eq(Set['stats'])
62
73
  end
63
74
 
75
+ it 'is configured as read only' do
76
+ expect(subject.read_only?).to eq(true)
77
+ end
78
+
64
79
  it 'raises error on add' do
65
80
  expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
66
81
  end
@@ -0,0 +1,62 @@
1
+ RSpec.describe Flipper::Adapters::Strict do
2
+ let(:flipper) { Flipper.new(subject) }
3
+ let(:feature) { flipper[:unknown] }
4
+
5
+ it_should_behave_like 'a flipper adapter' do
6
+ subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
7
+ end
8
+
9
+ context "handler = :raise" do
10
+ subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
11
+
12
+ context "#get" do
13
+ it "raises an error for unknown feature" do
14
+ expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
15
+ end
16
+ end
17
+
18
+ context "#get_multi" do
19
+ it "raises an error for unknown feature" do
20
+ expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
21
+ end
22
+ end
23
+ end
24
+
25
+ context "handler = :warn" do
26
+ subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
27
+
28
+ context "#get" do
29
+ it "raises an error for unknown feature" do
30
+ expect(silence { subject.get(feature) }).to match(/Could not find feature "unknown"/)
31
+ end
32
+ end
33
+
34
+ context "#get_multi" do
35
+ it "raises an error for unknown feature" do
36
+ expect(silence { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
37
+ end
38
+ end
39
+ end
40
+
41
+ context "handler = Block" do
42
+ let(:unknown_features) { [] }
43
+ subject do
44
+ described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
45
+ end
46
+
47
+
48
+ context "#get" do
49
+ it "raises an error for unknown feature" do
50
+ subject.get(feature)
51
+ expect(unknown_features).to eq(["unknown"])
52
+ end
53
+ end
54
+
55
+ context "#get_multi" do
56
+ it "raises an error for unknown feature" do
57
+ subject.get_multi([flipper[:foo], flipper[:bar]])
58
+ expect(unknown_features).to eq(["foo", "bar"])
59
+ end
60
+ end
61
+ end
62
+ end
@@ -8,6 +8,8 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
8
8
  end
9
9
  let(:flipper) { Flipper.new(adapter) }
10
10
  let(:feature) { flipper[:search] }
11
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
12
+ let(:age_expression) { Flipper.property(:age).gte(21) }
11
13
 
12
14
  context "when remote disabled" do
13
15
  let(:remote) { Flipper::GateValues.new({}) }
@@ -63,6 +65,7 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
63
65
  boolean: nil,
64
66
  actors: Set["1"],
65
67
  groups: Set["staff"],
68
+ expression: plan_expression.value,
66
69
  percentage_of_time: 10,
67
70
  percentage_of_actors: 15,
68
71
  }
@@ -74,10 +77,34 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
74
77
  expect(local_gate_values_hash.fetch(:boolean)).to be(nil)
75
78
  expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"])
76
79
  expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"])
80
+ expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value)
77
81
  expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10")
78
82
  expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15")
79
83
  end
80
84
 
85
+ it "updates expression when remote is updated" do
86
+ any_expression = Flipper.any(plan_expression, age_expression)
87
+ remote = Flipper::GateValues.new(expression: any_expression.value)
88
+ feature.enable_expression(age_expression)
89
+ adapter.reset
90
+
91
+ described_class.new(feature, feature.gate_values, remote).call
92
+
93
+ expect(feature.expression_value).to eq(any_expression.value)
94
+ expect_only_enable
95
+ end
96
+
97
+ it "does nothing to expression if in sync" do
98
+ remote = Flipper::GateValues.new(expression: plan_expression.value)
99
+ feature.enable_expression(plan_expression)
100
+ adapter.reset
101
+
102
+ described_class.new(feature, feature.gate_values, remote).call
103
+
104
+ expect(feature.expression_value).to eq(plan_expression.value)
105
+ expect_no_enable_or_disable
106
+ end
107
+
81
108
  it "adds remotely added actors" do
82
109
  remote = Flipper::GateValues.new(actors: Set["1", "2"])
83
110
  feature.enable_actor(Flipper::Actor.new("1"))
@@ -61,14 +61,14 @@ RSpec.describe Flipper::Cloud::Configuration do
61
61
  end
62
62
 
63
63
  it "can set sync_interval" do
64
- instance = described_class.new(required_options.merge(sync_interval: 1))
65
- expect(instance.sync_interval).to eq(1)
64
+ instance = described_class.new(required_options.merge(sync_interval: 15))
65
+ expect(instance.sync_interval).to eq(15)
66
66
  end
67
67
 
68
68
  it "can set sync_interval from ENV var" do
69
- with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "5" do
69
+ with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "15" do
70
70
  instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
71
- expect(instance.sync_interval).to eq(5)
71
+ expect(instance.sync_interval).to eq(15)
72
72
  end
73
73
  end
74
74
 
@@ -76,9 +76,9 @@ RSpec.describe Flipper::Cloud::Configuration do
76
76
  # The initial sync of http to local invokes this web request.
77
77
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
78
78
 
79
- instance = described_class.new(required_options.merge(sync_interval: 1))
79
+ instance = described_class.new(required_options.merge(sync_interval: 20))
80
80
  poller = instance.send(:poller)
81
- expect(poller.interval).to eq(1)
81
+ expect(poller.interval).to eq(20)
82
82
  end
83
83
 
84
84
  it "can set debug_output" do
@@ -249,21 +249,4 @@ RSpec.describe Flipper::Cloud::Configuration do
249
249
  expect(all["search"][:boolean]).to eq("true")
250
250
  expect(all["history"][:boolean]).to eq(nil)
251
251
  end
252
-
253
- it "can setup brow to report events to cloud" do
254
- # skip logging brow
255
- Brow.logger = Logger.new(File::NULL)
256
- brow = described_class.new(required_options).brow
257
-
258
- stub = stub_request(:post, "https://www.flippercloud.io/adapter/events")
259
- .with { |request|
260
- data = JSON.parse(request.body)
261
- data.keys == ["uuid", "messages"] && data["messages"] == [{"n" => 1}]
262
- }
263
- .to_return(status: 201, body: "{}", headers: {})
264
-
265
- brow.push({"n" => 1})
266
- brow.worker.stop
267
- expect(stub).to have_been_requested.times(1)
268
- end
269
252
  end
@@ -0,0 +1,108 @@
1
+ require 'flipper/cloud/telemetry/backoff_policy'
2
+
3
+ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
4
+ context "#initialize" do
5
+ it "with no options" do
6
+ policy = described_class.new
7
+ expect(policy.min_timeout_ms).to eq(1_000)
8
+ expect(policy.max_timeout_ms).to eq(30_000)
9
+ expect(policy.multiplier).to eq(1.5)
10
+ expect(policy.randomization_factor).to eq(0.5)
11
+ end
12
+
13
+ it "with options" do
14
+ policy = described_class.new({
15
+ min_timeout_ms: 1234,
16
+ max_timeout_ms: 5678,
17
+ multiplier: 24,
18
+ randomization_factor: 0.4,
19
+ })
20
+ expect(policy.min_timeout_ms).to eq(1234)
21
+ expect(policy.max_timeout_ms).to eq(5678)
22
+ expect(policy.multiplier).to eq(24)
23
+ expect(policy.randomization_factor).to eq(0.4)
24
+ end
25
+
26
+ it "with min higher than max" do
27
+ expect {
28
+ described_class.new({
29
+ min_timeout_ms: 2,
30
+ max_timeout_ms: 1,
31
+ })
32
+ }.to raise_error(ArgumentError, ":min_timeout_ms (2) must be <= :max_timeout_ms (1)")
33
+ end
34
+
35
+ it "with invalid min_timeout_ms" do
36
+ expect {
37
+ described_class.new({
38
+ min_timeout_ms: -1,
39
+ })
40
+ }.to raise_error(ArgumentError, ":min_timeout_ms must be >= 0 but was -1")
41
+ end
42
+
43
+ it "with invalid max_timeout_ms" do
44
+ expect {
45
+ described_class.new({
46
+ max_timeout_ms: -1,
47
+ })
48
+ }.to raise_error(ArgumentError, ":max_timeout_ms must be >= 0 but was -1")
49
+ end
50
+
51
+ it "from env" do
52
+ env = {
53
+ "FLIPPER_BACKOFF_MIN_TIMEOUT_MS" => "1000",
54
+ "FLIPPER_BACKOFF_MAX_TIMEOUT_MS" => "2000",
55
+ "FLIPPER_BACKOFF_MULTIPLIER" => "1.9",
56
+ "FLIPPER_BACKOFF_RANDOMIZATION_FACTOR" => "0.1",
57
+ }
58
+ with_env env do
59
+ policy = described_class.new
60
+ expect(policy.min_timeout_ms).to eq(1000)
61
+ expect(policy.max_timeout_ms).to eq(2000)
62
+ expect(policy.multiplier).to eq(1.9)
63
+ expect(policy.randomization_factor).to eq(0.1)
64
+ end
65
+ end
66
+ end
67
+
68
+ context "#next_interval" do
69
+ it "works" do
70
+ policy = described_class.new({
71
+ min_timeout_ms: 1_000,
72
+ max_timeout_ms: 10_000,
73
+ multiplier: 2,
74
+ randomization_factor: 0.5,
75
+ })
76
+
77
+ expect(policy.next_interval).to be_within(500).of(1000)
78
+ expect(policy.next_interval).to be_within(1000).of(2000)
79
+ expect(policy.next_interval).to be_within(2000).of(4000)
80
+ expect(policy.next_interval).to be_within(4000).of(8000)
81
+ end
82
+
83
+ it "caps maximum duration at max_timeout_secs" do
84
+ policy = described_class.new({
85
+ min_timeout_ms: 1_000,
86
+ max_timeout_ms: 10_000,
87
+ multiplier: 2,
88
+ randomization_factor: 0.5,
89
+ })
90
+ 10.times { policy.next_interval }
91
+ expect(policy.next_interval).to eq(10_000)
92
+ end
93
+ end
94
+
95
+ it "can reset" do
96
+ policy = described_class.new({
97
+ min_timeout_ms: 1_000,
98
+ max_timeout_ms: 10_000,
99
+ multiplier: 2,
100
+ randomization_factor: 0.5,
101
+ })
102
+ 10.times { policy.next_interval }
103
+
104
+ expect(policy.attempts).to eq(10)
105
+ policy.reset
106
+ expect(policy.attempts).to eq(0)
107
+ end
108
+ end