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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +50 -7
- data/.github/workflows/examples.yml +50 -8
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -584
- data/Gemfile +15 -8
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +6 -3
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +19 -14
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +131 -54
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +42 -3
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +9 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +4 -1
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +9 -8
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
- data/lib/flipper/test/shared_adapter_test.rb +41 -22
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +41 -2
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +135 -74
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +166 -0
- data/spec/flipper/cloud/configuration_spec.rb +39 -57
- data/spec/flipper/cloud/dsl_spec.rb +6 -6
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +31 -25
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -3
- data/spec/flipper/engine_spec.rb +226 -42
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +380 -10
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +91 -3
- data/spec/spec_helper.rb +17 -5
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +145 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
65
|
-
expect(instance.sync_interval).to eq(
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
75
|
+
instance = described_class.new(required_options.merge(sync_interval: 20))
|
|
80
76
|
poller = instance.send(:poller)
|
|
81
|
-
expect(poller.interval).to eq(
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
147
|
-
|
|
147
|
+
ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "abc"
|
|
148
|
+
instance = described_class.new(required_options)
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
'
|
|
235
|
+
'flipper-cloud-token'=>'asdf',
|
|
237
236
|
},
|
|
238
|
-
}).to_return(status: 200, body: body
|
|
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
|
-
'
|
|
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
|
-
|
|
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: {'
|
|
69
|
-
to_return(status: 200, body: '{}'
|
|
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: {'
|
|
72
|
-
to_return(status: 200, body: '{}'
|
|
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["
|
|
105
|
-
expect(last_response.headers["
|
|
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["
|
|
128
|
-
expect(last_response.headers["
|
|
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["
|
|
151
|
-
expect(last_response.headers["
|
|
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
|
-
'
|
|
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
|
|
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
|