flipper 1.0.0 → 1.1.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.
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