flipper 1.0.0 → 1.3.6

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -0,0 +1,166 @@
1
+ require "flipper/cli"
2
+
3
+ RSpec.describe Flipper::CLI do
4
+ let(:stdout) { StringIO.new }
5
+ let(:stderr) { StringIO.new }
6
+ let(:cli) { Flipper::CLI.new(stdout: stdout, stderr: stderr) }
7
+
8
+ Result = Struct.new(:status, :stdout, :stderr, keyword_init: true)
9
+
10
+ before do
11
+ # Prentend stdout/stderr a TTY to test colorization
12
+ allow(stdout).to receive(:tty?).and_return(true)
13
+ allow(stderr).to receive(:tty?).and_return(true)
14
+ end
15
+
16
+ # Infer the command from the description
17
+ let(:argv) do
18
+ descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1)
19
+ descriptions.map { |arg| Shellwords.split(arg) }.flatten
20
+ end
21
+
22
+ subject do
23
+ status = 0
24
+
25
+ begin
26
+ cli.run(argv)
27
+ rescue SystemExit => e
28
+ status = e.status
29
+ end
30
+
31
+ Result.new(status: status, stdout: stdout.string, stderr: stderr.string)
32
+ end
33
+
34
+ before do
35
+ ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment"
36
+ end
37
+
38
+ describe "enable" do
39
+ describe "feature" do
40
+ it do
41
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[32m.*enabled/)
42
+ expect(Flipper).to be_enabled(:feature)
43
+ end
44
+ end
45
+
46
+ describe "-a User;1 feature" do
47
+ it do
48
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[33m.*enabled.*User;1/m)
49
+ expect(Flipper).to be_enabled(:feature, Flipper::Actor.new("User;1"))
50
+ end
51
+ end
52
+
53
+ describe "feature -g admins" do
54
+ it do
55
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*admins/m)
56
+ expect(Flipper.feature('feature').enabled_groups.map(&:name)).to eq([:admins])
57
+ end
58
+ end
59
+
60
+ describe "feature -p 30" do
61
+ it do
62
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*30% of actors/m)
63
+ expect(Flipper.feature('feature').percentage_of_actors_value).to eq(30)
64
+ end
65
+ end
66
+
67
+ describe "feature -t 50" do
68
+ it do
69
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*50% of time/m)
70
+ expect(Flipper.feature('feature').percentage_of_time_value).to eq(50)
71
+ end
72
+ end
73
+
74
+ describe %|feature -x '{"Equal":[{"Property":"flipper_id"},"User;1"]}'| do
75
+ it do
76
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*User;1/m)
77
+ expect(Flipper.feature('feature').expression.value).to eq({ "Equal" => [ { "Property" => ["flipper_id"] }, "User;1" ] })
78
+ end
79
+ end
80
+
81
+ describe %|feature -x invalid_json| do
82
+ it do
83
+ expect(subject).to have_attributes(status: 1, stderr: /JSON parse error/m)
84
+ end
85
+ end
86
+
87
+ describe %|feature -x '{}'| do
88
+ it do
89
+ expect(subject).to have_attributes(status: 1, stderr: /Invalid expression/m)
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "disable" do
95
+ describe "feature" do
96
+ before { Flipper.enable :feature }
97
+
98
+ it do
99
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
100
+ expect(Flipper).not_to be_enabled(:feature)
101
+ end
102
+ end
103
+
104
+ describe "feature -g admins" do
105
+ before { Flipper.enable_group(:feature, :admins) }
106
+
107
+ it do
108
+ expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
109
+ expect(Flipper.feature('feature').enabled_groups).to be_empty
110
+ end
111
+ end
112
+ end
113
+
114
+ describe "list" do
115
+ before do
116
+ Flipper.enable :foo
117
+ Flipper.disable :bar
118
+ end
119
+
120
+ it "lists features" do
121
+ expect(subject).to have_attributes(status: 0, stdout: /foo.*enabled/)
122
+ expect(subject).to have_attributes(status: 0, stdout: /bar.*disabled/)
123
+ end
124
+ end
125
+
126
+ ["-h", "--help", "help"].each do |arg|
127
+ describe arg do
128
+ it { should have_attributes(status: 0, stdout: /Usage: flipper/) }
129
+
130
+ it "should list subcommands" do
131
+ %w(enable disable list).each do |subcommand|
132
+ expect(subject.stdout).to match(/#{subcommand}/)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "help enable" do
139
+ it { should have_attributes(status: 0, stdout: /Usage: flipper enable \[options\] <feature>/) }
140
+ end
141
+
142
+ describe "nope" do
143
+ it { should have_attributes(status: 1, stderr: /Unknown command: nope/) }
144
+ end
145
+
146
+ describe "--nope" do
147
+ it { should have_attributes(status: 1, stderr: /invalid option: --nope/) }
148
+ end
149
+
150
+ describe "show foo" do
151
+ context "boolean" do
152
+ before { Flipper.enable :foo }
153
+ it { should have_attributes(status: 0, stdout: /foo.*enabled/) }
154
+ end
155
+
156
+ context "actors" do
157
+ before { Flipper.enable_actor :foo, Flipper::Actor.new("User;1") }
158
+ it { should have_attributes(status: 0, stdout: /User;1/) }
159
+ end
160
+
161
+ context "groups" do
162
+ before { Flipper.enable_group :foo, :admins }
163
+ it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
164
+ end
165
+ end
166
+ end
@@ -12,16 +12,16 @@ RSpec.describe Flipper::Cloud::Configuration do
12
12
  end
13
13
 
14
14
  it "can set token from ENV var" do
15
- with_env "FLIPPER_CLOUD_TOKEN" => "from_env" do
16
- instance = described_class.new(required_options.reject { |k, v| k == :token })
17
- expect(instance.token).to eq("from_env")
18
- end
15
+ ENV["FLIPPER_CLOUD_TOKEN"] = "from_env"
16
+ instance = described_class.new(required_options.reject { |k, v| k == :token })
17
+ expect(instance.token).to eq("from_env")
19
18
  end
20
19
 
21
20
  it "can set instrumenter" do
22
21
  instrumenter = Object.new
23
22
  instance = described_class.new(required_options.merge(instrumenter: instrumenter))
24
- expect(instance.instrumenter).to be(instrumenter)
23
+ expect(instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
24
+ expect(instance.instrumenter.instrumenter).to be(instrumenter)
25
25
  end
26
26
 
27
27
  it "can set read_timeout" do
@@ -30,10 +30,9 @@ RSpec.describe Flipper::Cloud::Configuration do
30
30
  end
31
31
 
32
32
  it "can set read_timeout from ENV var" do
33
- with_env "FLIPPER_CLOUD_READ_TIMEOUT" => "9" do
34
- instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
35
- expect(instance.read_timeout).to eq(9)
36
- end
33
+ ENV["FLIPPER_CLOUD_READ_TIMEOUT"] = "9"
34
+ instance = described_class.new(required_options.reject { |k, v| k == :read_timeout })
35
+ expect(instance.read_timeout).to eq(9)
37
36
  end
38
37
 
39
38
  it "can set open_timeout" do
@@ -42,10 +41,9 @@ RSpec.describe Flipper::Cloud::Configuration do
42
41
  end
43
42
 
44
43
  it "can set open_timeout from ENV var" do
45
- with_env "FLIPPER_CLOUD_OPEN_TIMEOUT" => "9" do
46
- instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
47
- expect(instance.open_timeout).to eq(9)
48
- end
44
+ ENV["FLIPPER_CLOUD_OPEN_TIMEOUT"] = "9"
45
+ instance = described_class.new(required_options.reject { |k, v| k == :open_timeout })
46
+ expect(instance.open_timeout).to eq(9)
49
47
  end
50
48
 
51
49
  it "can set write_timeout" do
@@ -54,31 +52,29 @@ RSpec.describe Flipper::Cloud::Configuration do
54
52
  end
55
53
 
56
54
  it "can set write_timeout from ENV var" do
57
- with_env "FLIPPER_CLOUD_WRITE_TIMEOUT" => "9" do
58
- instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
59
- expect(instance.write_timeout).to eq(9)
60
- end
55
+ ENV["FLIPPER_CLOUD_WRITE_TIMEOUT"] = "9"
56
+ instance = described_class.new(required_options.reject { |k, v| k == :write_timeout })
57
+ expect(instance.write_timeout).to eq(9)
61
58
  end
62
59
 
63
60
  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)
61
+ instance = described_class.new(required_options.merge(sync_interval: 15))
62
+ expect(instance.sync_interval).to eq(15)
66
63
  end
67
64
 
68
65
  it "can set sync_interval from ENV var" do
69
- with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "5" do
70
- instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
71
- expect(instance.sync_interval).to eq(5)
72
- end
66
+ ENV["FLIPPER_CLOUD_SYNC_INTERVAL"] = "15"
67
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
68
+ expect(instance.sync_interval).to eq(15)
73
69
  end
74
70
 
75
71
  it "passes sync_interval into sync adapter" do
76
72
  # The initial sync of http to local invokes this web request.
77
73
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
78
74
 
79
- instance = described_class.new(required_options.merge(sync_interval: 1))
75
+ instance = described_class.new(required_options.merge(sync_interval: 20))
80
76
  poller = instance.send(:poller)
81
- expect(poller.interval).to eq(1)
77
+ expect(poller.interval).to eq(20)
82
78
  end
83
79
 
84
80
  it "can set debug_output" do
@@ -86,6 +82,12 @@ RSpec.describe Flipper::Cloud::Configuration do
86
82
  expect(instance.debug_output).to eq(STDOUT)
87
83
  end
88
84
 
85
+ it "defaults debug_output to STDOUT if FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT set to true" do
86
+ ENV["FLIPPER_CLOUD_DEBUG_OUTPUT_STDOUT"] = "true"
87
+ instance = described_class.new(required_options)
88
+ expect(instance.debug_output).to eq(STDOUT)
89
+ end
90
+
89
91
  it "defaults adapter block" do
90
92
  # The initial sync of http to local invokes this web request.
91
93
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
@@ -121,10 +123,9 @@ RSpec.describe Flipper::Cloud::Configuration do
121
123
  end
122
124
 
123
125
  it "can override URL using ENV var" do
124
- with_env "FLIPPER_CLOUD_URL" => "https://example.com" do
125
- instance = described_class.new(required_options.reject { |k, v| k == :url })
126
- expect(instance.url).to eq("https://example.com")
127
- end
126
+ ENV["FLIPPER_CLOUD_URL"] = "https://example.com"
127
+ instance = described_class.new(required_options.reject { |k, v| k == :url })
128
+ expect(instance.url).to eq("https://example.com")
128
129
  end
129
130
 
130
131
  it "defaults sync_method to :poll" do
@@ -143,12 +144,11 @@ RSpec.describe Flipper::Cloud::Configuration do
143
144
  end
144
145
 
145
146
  it "sets sync_method to :webhook if FLIPPER_CLOUD_SYNC_SECRET set" do
146
- with_env "FLIPPER_CLOUD_SYNC_SECRET" => "abc" do
147
- instance = described_class.new(required_options)
147
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
148
+ instance = described_class.new(required_options)
148
149
 
149
- expect(instance.sync_method).to eq(:webhook)
150
- expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
151
- end
150
+ expect(instance.sync_method).to eq(:webhook)
151
+ expect(instance.adapter).to be_instance_of(Flipper::Adapters::DualWrite)
152
152
  end
153
153
 
154
154
  it "can set sync_secret" do
@@ -157,10 +157,9 @@ RSpec.describe Flipper::Cloud::Configuration do
157
157
  end
158
158
 
159
159
  it "can override sync_secret using ENV var" do
160
- with_env "FLIPPER_CLOUD_SYNC_SECRET" => "from_env" do
161
- instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
162
- expect(instance.sync_secret).to eq("from_env")
163
- end
160
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "from_env"
161
+ instance = described_class.new(required_options.reject { |k, v| k == :sync_secret })
162
+ expect(instance.sync_secret).to eq("from_env")
164
163
  end
165
164
 
166
165
  it "can sync with cloud" do
@@ -233,9 +232,9 @@ RSpec.describe Flipper::Cloud::Configuration do
233
232
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
234
233
  with({
235
234
  headers: {
236
- 'Flipper-Cloud-Token'=>'asdf',
235
+ 'flipper-cloud-token'=>'asdf',
237
236
  },
238
- }).to_return(status: 200, body: body, headers: {})
237
+ }).to_return(status: 200, body: body)
239
238
  instance = described_class.new(required_options)
240
239
  instance.sync
241
240
 
@@ -249,21 +248,4 @@ RSpec.describe Flipper::Cloud::Configuration do
249
248
  expect(all["search"][:boolean]).to eq("true")
250
249
  expect(all["history"][:boolean]).to eq(nil)
251
250
  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
251
  end
@@ -18,7 +18,7 @@ RSpec.describe Flipper::Cloud::DSL do
18
18
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
19
19
  with({
20
20
  headers: {
21
- 'Flipper-Cloud-Token'=>'asdf',
21
+ 'flipper-cloud-token'=>'asdf',
22
22
  },
23
23
  }).to_return(status: 200, body: '{"features": {}}', headers: {})
24
24
  cloud_configuration = Flipper::Cloud::Configuration.new({
@@ -45,7 +45,7 @@ RSpec.describe Flipper::Cloud::DSL do
45
45
  end
46
46
 
47
47
  let(:cloud_configuration) do
48
- cloud_configuration = Flipper::Cloud::Configuration.new({
48
+ Flipper::Cloud::Configuration.new({
49
49
  token: "asdf",
50
50
  sync_secret: "tasty",
51
51
  local_adapter: local_adapter
@@ -65,11 +65,11 @@ RSpec.describe Flipper::Cloud::DSL do
65
65
 
66
66
  it "sends writes to cloud and local" do
67
67
  add_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features").
68
- with({headers: {'Flipper-Cloud-Token'=>'asdf'}}).
69
- to_return(status: 200, body: '{}', headers: {})
68
+ with({headers: {'flipper-cloud-token'=>'asdf'}}).
69
+ to_return(status: 200, body: '{}')
70
70
  enable_stub = stub_request(:post, "https://www.flippercloud.io/adapter/features/foo/boolean").
71
- with(headers: {'Flipper-Cloud-Token'=>'asdf'}).
72
- to_return(status: 200, body: '{}', headers: {})
71
+ with(headers: {'flipper-cloud-token'=>'asdf'}).
72
+ to_return(status: 200, body: '{}')
73
73
 
74
74
  subject.enable(:foo)
75
75
 
@@ -101,8 +101,8 @@ RSpec.describe Flipper::Cloud::Middleware do
101
101
  post '/', request_body, env
102
102
 
103
103
  expect(last_response.status).to eq(402)
104
- expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
105
- expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 402")
104
+ expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
105
+ expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 402")
106
106
  expect(stub).to have_been_requested
107
107
  end
108
108
  end
@@ -124,8 +124,8 @@ RSpec.describe Flipper::Cloud::Middleware do
124
124
  post '/', request_body, env
125
125
 
126
126
  expect(last_response.status).to eq(500)
127
- expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Flipper::Adapters::Http::Error")
128
- expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to include("Failed with status: 503")
127
+ expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Flipper::Adapters::Http::Error")
128
+ expect(last_response.headers["flipper-cloud-response-error-message"]).to include("Failed with status: 503")
129
129
  expect(stub).to have_been_requested
130
130
  end
131
131
  end
@@ -147,8 +147,8 @@ RSpec.describe Flipper::Cloud::Middleware do
147
147
  post '/', request_body, env
148
148
 
149
149
  expect(last_response.status).to eq(500)
150
- expect(last_response.headers["Flipper-Cloud-Response-Error-Class"]).to eq("Net::OpenTimeout")
151
- expect(last_response.headers["Flipper-Cloud-Response-Error-Message"]).to eq("execution expired")
150
+ expect(last_response.headers["flipper-cloud-response-error-class"]).to eq("Net::OpenTimeout")
151
+ expect(last_response.headers["flipper-cloud-response-error-message"]).to eq("execution expired")
152
152
  expect(stub).to have_been_requested
153
153
  end
154
154
  end
@@ -277,13 +277,13 @@ RSpec.describe Flipper::Cloud::Middleware do
277
277
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
278
278
  with({
279
279
  headers: {
280
- 'Flipper-Cloud-Token' => token,
280
+ 'flipper-cloud-token' => token,
281
281
  },
282
282
  })
283
283
  if status == :timeout
284
284
  stub.to_timeout
285
285
  else
286
- stub.to_return(status: status, body: response_body, headers: {})
286
+ stub.to_return(status: status, body: response_body)
287
287
  end
288
288
  end
289
289
  end
@@ -0,0 +1,107 @@
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(30_000)
8
+ expect(policy.max_timeout_ms).to eq(120_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.update(
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
+
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
+
67
+ context "#next_interval" do
68
+ it "works" do
69
+ policy = described_class.new({
70
+ min_timeout_ms: 1_000,
71
+ max_timeout_ms: 10_000,
72
+ multiplier: 2,
73
+ randomization_factor: 0.5,
74
+ })
75
+
76
+ expect(policy.next_interval).to be_within(500).of(1000)
77
+ expect(policy.next_interval).to be_within(1000).of(2000)
78
+ expect(policy.next_interval).to be_within(2000).of(4000)
79
+ expect(policy.next_interval).to be_within(4000).of(8000)
80
+ end
81
+
82
+ it "caps maximum duration at max_timeout_secs" do
83
+ policy = described_class.new({
84
+ min_timeout_ms: 1_000,
85
+ max_timeout_ms: 10_000,
86
+ multiplier: 2,
87
+ randomization_factor: 0.5,
88
+ })
89
+ 10.times { policy.next_interval }
90
+ expect(policy.next_interval).to be_within(10_000*0.1).of(10_000)
91
+ end
92
+ end
93
+
94
+ it "can reset" do
95
+ policy = described_class.new({
96
+ min_timeout_ms: 1_000,
97
+ max_timeout_ms: 10_000,
98
+ multiplier: 2,
99
+ randomization_factor: 0.5,
100
+ })
101
+ 10.times { policy.next_interval }
102
+
103
+ expect(policy.attempts).to eq(10)
104
+ policy.reset
105
+ expect(policy.attempts).to eq(0)
106
+ end
107
+ end
@@ -0,0 +1,87 @@
1
+ require 'flipper/cloud/telemetry/metric'
2
+
3
+ RSpec.describe Flipper::Cloud::Telemetry::Metric do
4
+ it 'has key, result and time' do
5
+ metric = described_class.new(:search, true, 1696793160)
6
+ expect(metric.key).to eq(:search)
7
+ expect(metric.result).to eq(true)
8
+ expect(metric.time).to eq(1696793160)
9
+ end
10
+
11
+ it "clamps time to minute" do
12
+ metric = described_class.new(:search, true, 1696793204)
13
+ expect(metric.time).to eq(1696793160)
14
+ end
15
+
16
+ describe "#eql?" do
17
+ it "returns true when key, time and result are the same" do
18
+ metric = described_class.new(:search, true, 1696793204)
19
+ other = described_class.new(:search, true, 1696793204)
20
+ expect(metric.eql?(other)).to be(true)
21
+ end
22
+
23
+ it "returns false for other class" do
24
+ metric = described_class.new(:search, true, 1696793204)
25
+ other = Object.new
26
+ expect(metric.eql?(other)).to be(false)
27
+ end
28
+
29
+ it "returns false for sub class" do
30
+ metric = described_class.new(:search, true, 1696793204)
31
+ other = Class.new(described_class).new(:search, true, 1696793204)
32
+ expect(metric.eql?(other)).to be(false)
33
+ end
34
+
35
+ it "returns false if key is different" do
36
+ metric = described_class.new(:search, true, 1696793204)
37
+ other = described_class.new(:other, true, 1696793204)
38
+ expect(metric.eql?(other)).to be(false)
39
+ end
40
+
41
+ it "returns false if time is different" do
42
+ metric = described_class.new(:search, true, 1696793204)
43
+ other = described_class.new(:search, true, 1696793204 - 60 - 60)
44
+ expect(metric.eql?(other)).to be(false)
45
+ end
46
+
47
+ it "returns true with different times if times are in same minute" do
48
+ metric = described_class.new(:search, true, 1696793204)
49
+ other = described_class.new(:search, true, 1696793206)
50
+ expect(metric.eql?(other)).to be(true)
51
+ end
52
+
53
+ it "returns false if result is different" do
54
+ metric = described_class.new(:search, true, 1696793204)
55
+ other = described_class.new(:search, false, 1696793204)
56
+ expect(metric.eql?(other)).to be(false)
57
+ end
58
+ end
59
+
60
+ describe "#hash" do
61
+ it "returns hash based on class, key, time and result" do
62
+ metric = described_class.new(:search, true, 1696793204)
63
+ expect(metric.hash).to eq([described_class, metric.key, metric.time, metric.result].hash)
64
+ end
65
+ end
66
+
67
+ describe "#as_json" do
68
+ it "returns key time and result" do
69
+ metric = described_class.new(:search, true, 1696793160)
70
+ expect(metric.as_json).to eq({
71
+ "key" => "search",
72
+ "result" => true,
73
+ "time" => 1696793160,
74
+ })
75
+ end
76
+
77
+ it "can include other hashes" do
78
+ metric = described_class.new(:search, true, 1696793160)
79
+ expect(metric.as_json(with: {"value" => 2})).to eq({
80
+ "key" => "search",
81
+ "result" => true,
82
+ "time" => 1696793160,
83
+ "value" => 2,
84
+ })
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ require 'flipper/cloud/telemetry/metric_storage'
2
+ require 'flipper/cloud/telemetry/metric'
3
+
4
+ RSpec.describe Flipper::Cloud::Telemetry::MetricStorage do
5
+ describe "#increment" do
6
+ it "increments the counter for the metric" do
7
+ metric_storage = described_class.new
8
+ storage = metric_storage.instance_variable_get(:@storage)
9
+ metric = Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)
10
+ other = Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793160)
11
+
12
+ metric_storage.increment(metric)
13
+ expect(storage[metric].value).to be(1)
14
+
15
+ 5.times { metric_storage.increment(metric) }
16
+ expect(storage[metric].value).to be(6)
17
+
18
+ metric_storage.increment(other)
19
+ expect(storage[other].value).to be(1)
20
+ end
21
+ end
22
+
23
+ describe "#drain" do
24
+ it "returns clears metrics and return hash" do
25
+ metric_storage = described_class.new
26
+ storage = metric_storage.instance_variable_get(:@storage)
27
+ storage[Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)] = Concurrent::AtomicFixnum.new(10)
28
+ storage[Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793161)] = Concurrent::AtomicFixnum.new(15)
29
+ storage[Flipper::Cloud::Telemetry::Metric.new(:plausible, true, 1696793162)] = Concurrent::AtomicFixnum.new(25)
30
+ storage[Flipper::Cloud::Telemetry::Metric.new(:administrator, true, 1696793164)] = Concurrent::AtomicFixnum.new(1)
31
+ storage[Flipper::Cloud::Telemetry::Metric.new(:administrator, false, 1696793164)] = Concurrent::AtomicFixnum.new(24)
32
+
33
+ drained = metric_storage.drain
34
+ expect(drained).to be_frozen
35
+ expect(drained).to eq({
36
+ Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160) => 10,
37
+ Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793161) => 15,
38
+ Flipper::Cloud::Telemetry::Metric.new(:plausible, true, 1696793162) => 25,
39
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, true, 1696793164) => 1,
40
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, false, 1696793164) => 24,
41
+ })
42
+ expect(storage.keys).to eq([])
43
+ end
44
+ end
45
+
46
+ describe "#empty?" do
47
+ it "returns true if empty" do
48
+ metric_storage = described_class.new
49
+ expect(metric_storage).to be_empty
50
+ end
51
+
52
+ it "returns false if not empty" do
53
+ metric_storage = described_class.new
54
+ metric_storage.increment Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)
55
+ expect(metric_storage).not_to be_empty
56
+ end
57
+ end
58
+ end