flipper 1.1.2 → 1.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -2
- data/.github/workflows/examples.yml +8 -2
- data/Changelog.md +1 -647
- data/Gemfile +3 -2
- data/README.md +3 -1
- data/Rakefile +2 -2
- data/docs/images/banner.jpg +0 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +5 -1
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/http/client.rb +25 -16
- data/lib/flipper/adapters/operation_logger.rb +18 -88
- data/lib/flipper/adapters/read_only.rb +6 -39
- data/lib/flipper/adapters/strict.rb +16 -18
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +9 -4
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
- data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
- data/lib/flipper/cloud/telemetry.rb +10 -2
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/engine.rb +32 -17
- data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
- data/lib/flipper/metadata.rb +3 -1
- data/lib/flipper/poller.rb +6 -5
- data/lib/flipper/serializers/gzip.rb +3 -5
- data/lib/flipper/serializers/json.rb +3 -5
- data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
- data/lib/flipper/test/shared_adapter_test.rb +17 -17
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +3 -3
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +3 -1
- data/lib/generators/flipper/setup_generator.rb +63 -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 +1 -2
- 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 +102 -76
- data/spec/flipper/adapters/strict_spec.rb +11 -9
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +35 -36
- data/spec/flipper/cloud/dsl_spec.rb +5 -5
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
- data/spec/flipper/cloud/telemetry_spec.rb +53 -1
- data/spec/flipper/cloud_spec.rb +10 -9
- data/spec/flipper/engine_spec.rb +140 -58
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
- data/spec/flipper/middleware/memoizer_spec.rb +7 -4
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/spec_helpers.rb +12 -5
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +31 -9
- data/spec/support/climate_control.rb +0 -7
@@ -0,0 +1,164 @@
|
|
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
|
+
before do
|
9
|
+
# Prentend stdout/stderr a TTY to test colorization
|
10
|
+
allow(stdout).to receive(:tty?).and_return(true)
|
11
|
+
allow(stderr).to receive(:tty?).and_return(true)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Infer the command from the description
|
15
|
+
subject(:argv) do
|
16
|
+
descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1)
|
17
|
+
descriptions.map { |arg| Shellwords.split(arg) }.flatten
|
18
|
+
end
|
19
|
+
|
20
|
+
subject do
|
21
|
+
status = 0
|
22
|
+
|
23
|
+
begin
|
24
|
+
cli.run(argv)
|
25
|
+
rescue SystemExit => e
|
26
|
+
status = e.status
|
27
|
+
end
|
28
|
+
|
29
|
+
OpenStruct.new(status: status, stdout: stdout.string, stderr: stderr.string)
|
30
|
+
end
|
31
|
+
|
32
|
+
before do
|
33
|
+
ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment"
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "enable" do
|
37
|
+
describe "feature" do
|
38
|
+
it do
|
39
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[32m.*enabled/)
|
40
|
+
expect(Flipper).to be_enabled(:feature)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "-a User;1 feature" do
|
45
|
+
it do
|
46
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*\e\[33m.*enabled.*User;1/m)
|
47
|
+
expect(Flipper).to be_enabled(:feature, Flipper::Actor.new("User;1"))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "feature -g admins" do
|
52
|
+
it do
|
53
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*admins/m)
|
54
|
+
expect(Flipper.feature('feature').enabled_groups.map(&:name)).to eq([:admins])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "feature -p 30" do
|
59
|
+
it do
|
60
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*30% of actors/m)
|
61
|
+
expect(Flipper.feature('feature').percentage_of_actors_value).to eq(30)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "feature -t 50" do
|
66
|
+
it do
|
67
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*50% of time/m)
|
68
|
+
expect(Flipper.feature('feature').percentage_of_time_value).to eq(50)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe %|feature -x '{"Equal":[{"Property":"flipper_id"},"User;1"]}'| do
|
73
|
+
it do
|
74
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*enabled.*User;1/m)
|
75
|
+
expect(Flipper.feature('feature').expression.value).to eq({ "Equal" => [ { "Property" => ["flipper_id"] }, "User;1" ] })
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe %|feature -x invalid_json| do
|
80
|
+
it do
|
81
|
+
expect(subject).to have_attributes(status: 1, stderr: /JSON parse error/m)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe %|feature -x '{}'| do
|
86
|
+
it do
|
87
|
+
expect(subject).to have_attributes(status: 1, stderr: /Invalid expression/m)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "disable" do
|
93
|
+
describe "feature" do
|
94
|
+
before { Flipper.enable :feature }
|
95
|
+
|
96
|
+
it do
|
97
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
|
98
|
+
expect(Flipper).not_to be_enabled(:feature)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "feature -g admins" do
|
103
|
+
before { Flipper.enable_group(:feature, :admins) }
|
104
|
+
|
105
|
+
it do
|
106
|
+
expect(subject).to have_attributes(status: 0, stdout: /feature.*disabled/)
|
107
|
+
expect(Flipper.feature('feature').enabled_groups).to be_empty
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "list" do
|
113
|
+
before do
|
114
|
+
Flipper.enable :foo
|
115
|
+
Flipper.disable :bar
|
116
|
+
end
|
117
|
+
|
118
|
+
it "lists features" do
|
119
|
+
expect(subject).to have_attributes(status: 0, stdout: /foo.*enabled/)
|
120
|
+
expect(subject).to have_attributes(status: 0, stdout: /bar.*disabled/)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
["-h", "--help", "help"].each do |arg|
|
125
|
+
describe arg do
|
126
|
+
it { should have_attributes(status: 0, stdout: /Usage: flipper/) }
|
127
|
+
|
128
|
+
it "should list subcommands" do
|
129
|
+
%w(enable disable list).each do |subcommand|
|
130
|
+
expect(subject.stdout).to match(/#{subcommand}/)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "help enable" do
|
137
|
+
it { should have_attributes(status: 0, stdout: /Usage: flipper enable \[options\] <feature>/) }
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "nope" do
|
141
|
+
it { should have_attributes(status: 1, stderr: /Unknown command: nope/) }
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "--nope" do
|
145
|
+
it { should have_attributes(status: 1, stderr: /invalid option: --nope/) }
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "show foo" do
|
149
|
+
context "boolean" do
|
150
|
+
before { Flipper.enable :foo }
|
151
|
+
it { should have_attributes(status: 0, stdout: /foo.*enabled/) }
|
152
|
+
end
|
153
|
+
|
154
|
+
context "actors" do
|
155
|
+
before { Flipper.enable_actor :foo, Flipper::Actor.new("User;1") }
|
156
|
+
it { should have_attributes(status: 0, stdout: /User;1/) }
|
157
|
+
end
|
158
|
+
|
159
|
+
context "groups" do
|
160
|
+
before { Flipper.enable_group :foo, :admins }
|
161
|
+
it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
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,10 +52,9 @@ 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
|
@@ -66,10 +63,9 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
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
|
@@ -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
|
|
@@ -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({
|
@@ -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
|
@@ -49,19 +49,18 @@ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
|
|
49
49
|
end
|
50
50
|
|
51
51
|
it "from env" do
|
52
|
-
|
52
|
+
ENV.update(
|
53
53
|
"FLIPPER_BACKOFF_MIN_TIMEOUT_MS" => "1000",
|
54
54
|
"FLIPPER_BACKOFF_MAX_TIMEOUT_MS" => "2000",
|
55
55
|
"FLIPPER_BACKOFF_MULTIPLIER" => "1.9",
|
56
56
|
"FLIPPER_BACKOFF_RANDOMIZATION_FACTOR" => "0.1",
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
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)
|
65
64
|
end
|
66
65
|
end
|
67
66
|
|
@@ -43,33 +43,33 @@ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
|
|
43
43
|
]
|
44
44
|
}
|
45
45
|
expected_headers = {
|
46
|
-
'
|
47
|
-
'
|
48
|
-
'
|
49
|
-
'
|
50
|
-
'
|
51
|
-
'
|
52
|
-
'
|
53
|
-
'
|
54
|
-
'
|
55
|
-
'
|
56
|
-
'
|
57
|
-
'
|
58
|
-
'
|
46
|
+
'accept' => 'application/json',
|
47
|
+
'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
|
48
|
+
'client-hostname' => Socket.gethostname,
|
49
|
+
'client-language' => 'ruby',
|
50
|
+
'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
|
51
|
+
'client-pid' => Process.pid.to_s,
|
52
|
+
'client-platform' => RUBY_PLATFORM,
|
53
|
+
'client-thread' => Thread.current.object_id.to_s,
|
54
|
+
'content-encoding' => 'gzip',
|
55
|
+
'content-type' => 'application/json',
|
56
|
+
'flipper-cloud-token' => 'asdf',
|
57
|
+
'schema-version' => 'V1',
|
58
|
+
'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
|
59
59
|
}
|
60
60
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
61
61
|
with(headers: expected_headers) { |request|
|
62
62
|
gunzipped = Flipper::Typecast.from_gzip(request.body)
|
63
63
|
body = Flipper::Typecast.from_json(gunzipped)
|
64
64
|
body == expected_body
|
65
|
-
}.to_return(status: 200, body: "{}"
|
65
|
+
}.to_return(status: 200, body: "{}")
|
66
66
|
subject.call(enabled_metrics)
|
67
67
|
end
|
68
68
|
|
69
69
|
it "defaults backoff_policy" do
|
70
70
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
71
|
-
to_return(status: 429, body: "{}"
|
72
|
-
to_return(status: 200, body: "{}"
|
71
|
+
to_return(status: 429, body: "{}").
|
72
|
+
to_return(status: 200, body: "{}")
|
73
73
|
instance = described_class.new(cloud_configuration)
|
74
74
|
expect(instance.backoff_policy.min_timeout_ms).to eq(1_000)
|
75
75
|
expect(instance.backoff_policy.max_timeout_ms).to eq(30_000)
|
@@ -77,7 +77,7 @@ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
|
|
77
77
|
|
78
78
|
it "tries 10 times by default" do
|
79
79
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
80
|
-
to_return(status: 500, body: "{}"
|
80
|
+
to_return(status: 500, body: "{}")
|
81
81
|
subject.call(enabled_metrics)
|
82
82
|
expect(subject.backoff_policy.retries).to eq(9) # 9 retries + 1 initial attempt
|
83
83
|
end
|
@@ -111,19 +111,19 @@ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
|
|
111
111
|
|
112
112
|
it "retries on 429" do
|
113
113
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
114
|
-
to_return(status: 429, body: "{}"
|
115
|
-
to_return(status: 429, body: "{}"
|
116
|
-
to_return(status: 200, body: "{}"
|
114
|
+
to_return(status: 429, body: "{}").
|
115
|
+
to_return(status: 429, body: "{}").
|
116
|
+
to_return(status: 200, body: "{}")
|
117
117
|
subject.call(enabled_metrics)
|
118
118
|
expect(subject.backoff_policy.retries).to eq(2)
|
119
119
|
end
|
120
120
|
|
121
121
|
it "retries on 500" do
|
122
122
|
stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
123
|
-
to_return(status: 500, body: "{}"
|
124
|
-
to_return(status: 503, body: "{}"
|
125
|
-
to_return(status: 502, body: "{}"
|
126
|
-
to_return(status: 200, body: "{}"
|
123
|
+
to_return(status: 500, body: "{}").
|
124
|
+
to_return(status: 503, body: "{}").
|
125
|
+
to_return(status: 502, body: "{}").
|
126
|
+
to_return(status: 200, body: "{}")
|
127
127
|
subject.call(enabled_metrics)
|
128
128
|
expect(subject.backoff_policy.retries).to eq(3)
|
129
129
|
end
|
@@ -2,9 +2,15 @@ require 'flipper/cloud/telemetry'
|
|
2
2
|
require 'flipper/cloud/configuration'
|
3
3
|
|
4
4
|
RSpec.describe Flipper::Cloud::Telemetry do
|
5
|
+
before do
|
6
|
+
# Stub polling for features.
|
7
|
+
stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
|
8
|
+
to_return(status: 200, body: "{}")
|
9
|
+
end
|
10
|
+
|
5
11
|
it "phones home and does not update telemetry interval if missing" do
|
6
12
|
stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
7
|
-
to_return(status: 200, body: "{}"
|
13
|
+
to_return(status: 200, body: "{}")
|
8
14
|
|
9
15
|
cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
|
10
16
|
|
@@ -42,6 +48,52 @@ RSpec.describe Flipper::Cloud::Telemetry do
|
|
42
48
|
expect(stub).to have_been_requested
|
43
49
|
end
|
44
50
|
|
51
|
+
it "phones home and requests shutdown if telemetry-shutdown header is true" do
|
52
|
+
stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
53
|
+
to_return(status: 404, body: "{}", headers: {"telemetry-shutdown" => "true"})
|
54
|
+
|
55
|
+
output = StringIO.new
|
56
|
+
cloud_configuration = Flipper::Cloud::Configuration.new(
|
57
|
+
token: "test",
|
58
|
+
logger: Logger.new(output),
|
59
|
+
logging_enabled: true,
|
60
|
+
)
|
61
|
+
|
62
|
+
# Record some telemetry and stop the threads so we submit a response.
|
63
|
+
telemetry = described_class.new(cloud_configuration)
|
64
|
+
telemetry.record(Flipper::Feature::InstrumentationName, {
|
65
|
+
operation: :enabled?,
|
66
|
+
feature_name: :foo,
|
67
|
+
result: true,
|
68
|
+
})
|
69
|
+
telemetry.stop
|
70
|
+
expect(stub).to have_been_requested
|
71
|
+
expect(output.string).to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "phones home and does not shutdown if telemetry shutdown header is missing" do
|
75
|
+
stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
76
|
+
to_return(status: 404, body: "{}", headers: {})
|
77
|
+
|
78
|
+
output = StringIO.new
|
79
|
+
cloud_configuration = Flipper::Cloud::Configuration.new(
|
80
|
+
token: "test",
|
81
|
+
logger: Logger.new(output),
|
82
|
+
logging_enabled: true,
|
83
|
+
)
|
84
|
+
|
85
|
+
# Record some telemetry and stop the threads so we submit a response.
|
86
|
+
telemetry = described_class.new(cloud_configuration)
|
87
|
+
telemetry.record(Flipper::Feature::InstrumentationName, {
|
88
|
+
operation: :enabled?,
|
89
|
+
feature_name: :foo,
|
90
|
+
result: true,
|
91
|
+
})
|
92
|
+
telemetry.stop
|
93
|
+
expect(stub).to have_been_requested
|
94
|
+
expect(output.string).not_to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
|
95
|
+
end
|
96
|
+
|
45
97
|
it "can update telemetry interval from error" do
|
46
98
|
stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
|
47
99
|
to_return(status: 500, body: "{}", headers: {"telemetry-interval" => "120"})
|
data/spec/flipper/cloud_spec.rb
CHANGED
@@ -35,8 +35,9 @@ RSpec.describe Flipper::Cloud do
|
|
35
35
|
expect(client.uri.scheme).to eq('https')
|
36
36
|
expect(client.uri.host).to eq('www.flippercloud.io')
|
37
37
|
expect(client.uri.path).to eq('/adapter')
|
38
|
-
expect(client.headers[
|
39
|
-
expect(@instance.instrumenter).to
|
38
|
+
expect(client.headers["flipper-cloud-token"]).to eq(token)
|
39
|
+
expect(@instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
|
40
|
+
expect(@instance.instrumenter.instrumenter).to be(Flipper::Instrumenters::Noop)
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
@@ -55,15 +56,15 @@ RSpec.describe Flipper::Cloud do
|
|
55
56
|
end
|
56
57
|
|
57
58
|
it 'can initialize with no token explicitly provided' do
|
58
|
-
|
59
|
-
|
60
|
-
end
|
59
|
+
ENV['FLIPPER_CLOUD_TOKEN'] = 'asdf'
|
60
|
+
expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'can set instrumenter' do
|
64
64
|
instrumenter = Flipper::Instrumenters::Memory.new
|
65
65
|
instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
|
66
|
-
expect(instance.instrumenter).to
|
66
|
+
expect(instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
|
67
|
+
expect(instance.instrumenter.instrumenter).to be(instrumenter)
|
67
68
|
end
|
68
69
|
|
69
70
|
it 'allows wrapping adapter with another adapter like the instrumenter' do
|
@@ -104,7 +105,7 @@ RSpec.describe Flipper::Cloud do
|
|
104
105
|
|
105
106
|
it 'can import' do
|
106
107
|
stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
|
107
|
-
with(headers: {'
|
108
|
+
with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
|
108
109
|
|
109
110
|
flipper = Flipper.new(Flipper::Adapters::Memory.new)
|
110
111
|
|
@@ -130,7 +131,7 @@ RSpec.describe Flipper::Cloud do
|
|
130
131
|
|
131
132
|
it 'raises error for failure while importing' do
|
132
133
|
stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
|
133
|
-
with(headers: {'
|
134
|
+
with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 500, body: "{}")
|
134
135
|
|
135
136
|
flipper = Flipper.new(Flipper::Adapters::Memory.new)
|
136
137
|
|
@@ -155,7 +156,7 @@ RSpec.describe Flipper::Cloud do
|
|
155
156
|
|
156
157
|
it 'raises error for timeout while importing' do
|
157
158
|
stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
|
158
|
-
with(headers: {'
|
159
|
+
with(headers: {'flipper-cloud-token'=>'asdf'}).to_timeout
|
159
160
|
|
160
161
|
flipper = Flipper.new(Flipper::Adapters::Memory.new)
|
161
162
|
|