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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -2
  3. data/.github/workflows/examples.yml +8 -2
  4. data/Changelog.md +1 -647
  5. data/Gemfile +3 -2
  6. data/README.md +3 -1
  7. data/Rakefile +2 -2
  8. data/docs/images/banner.jpg +0 -0
  9. data/exe/flipper +5 -0
  10. data/flipper.gemspec +5 -1
  11. data/lib/flipper/adapters/actor_limit.rb +28 -0
  12. data/lib/flipper/adapters/cache_base.rb +143 -0
  13. data/lib/flipper/adapters/http/client.rb +25 -16
  14. data/lib/flipper/adapters/operation_logger.rb +18 -88
  15. data/lib/flipper/adapters/read_only.rb +6 -39
  16. data/lib/flipper/adapters/strict.rb +16 -18
  17. data/lib/flipper/adapters/wrapper.rb +54 -0
  18. data/lib/flipper/cli.rb +263 -0
  19. data/lib/flipper/cloud/configuration.rb +9 -4
  20. data/lib/flipper/cloud/middleware.rb +5 -5
  21. data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
  22. data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
  23. data/lib/flipper/cloud/telemetry.rb +10 -2
  24. data/lib/flipper/cloud.rb +1 -1
  25. data/lib/flipper/engine.rb +32 -17
  26. data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
  27. data/lib/flipper/metadata.rb +3 -1
  28. data/lib/flipper/poller.rb +6 -5
  29. data/lib/flipper/serializers/gzip.rb +3 -5
  30. data/lib/flipper/serializers/json.rb +3 -5
  31. data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
  32. data/lib/flipper/test/shared_adapter_test.rb +17 -17
  33. data/lib/flipper/test_help.rb +43 -0
  34. data/lib/flipper/typecast.rb +3 -3
  35. data/lib/flipper/version.rb +11 -1
  36. data/lib/flipper.rb +3 -1
  37. data/lib/generators/flipper/setup_generator.rb +63 -0
  38. data/package-lock.json +41 -0
  39. data/package.json +10 -0
  40. data/spec/fixtures/environment.rb +1 -0
  41. data/spec/flipper/adapter_builder_spec.rb +1 -2
  42. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  43. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  44. data/spec/flipper/adapters/http_spec.rb +102 -76
  45. data/spec/flipper/adapters/strict_spec.rb +11 -9
  46. data/spec/flipper/cli_spec.rb +164 -0
  47. data/spec/flipper/cloud/configuration_spec.rb +35 -36
  48. data/spec/flipper/cloud/dsl_spec.rb +5 -5
  49. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  50. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
  51. data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
  52. data/spec/flipper/cloud/telemetry_spec.rb +53 -1
  53. data/spec/flipper/cloud_spec.rb +10 -9
  54. data/spec/flipper/engine_spec.rb +140 -58
  55. data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
  56. data/spec/flipper/middleware/memoizer_spec.rb +7 -4
  57. data/spec/flipper_spec.rb +1 -1
  58. data/spec/spec_helper.rb +1 -0
  59. data/spec/support/fail_on_output.rb +8 -0
  60. data/spec/support/spec_helpers.rb +12 -5
  61. data/test/adapters/actor_limit_test.rb +20 -0
  62. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  63. data/test_rails/system/test_help_test.rb +51 -0
  64. metadata +31 -9
  65. 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
- 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,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
- 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
@@ -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
- with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "15" do
70
- instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
71
- expect(instance.sync_interval).to eq(15)
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
- 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
 
@@ -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({
@@ -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
@@ -49,19 +49,18 @@ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
49
49
  end
50
50
 
51
51
  it "from env" do
52
- env = {
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
- with_env env do
59
- policy = described_class.new
60
- expect(policy.min_timeout_ms).to eq(1000)
61
- expect(policy.max_timeout_ms).to eq(2000)
62
- expect(policy.multiplier).to eq(1.9)
63
- expect(policy.randomization_factor).to eq(0.1)
64
- end
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
- '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}",
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: "{}", headers: {})
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: "{}", headers: {}).
72
- to_return(status: 200, body: "{}", headers: {})
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: "{}", headers: {})
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: "{}", headers: {}).
115
- to_return(status: 429, body: "{}", headers: {}).
116
- to_return(status: 200, body: "{}", headers: {})
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: "{}", headers: {}).
124
- to_return(status: 503, body: "{}", headers: {}).
125
- to_return(status: 502, body: "{}", headers: {}).
126
- to_return(status: 200, body: "{}", headers: {})
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: "{}", headers: {})
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"})
@@ -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['Flipper-Cloud-Token']).to eq(token)
39
- expect(@instance.instrumenter).to be(Flipper::Instrumenters::Noop)
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
- with_env 'FLIPPER_CLOUD_TOKEN' => 'asdf' do
59
- expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
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 be(instrumenter)
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: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 200, body: "{}", 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: {'Flipper-Cloud-Token'=>'asdf'}).to_return(status: 500, body: "{}")
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: {'Flipper-Cloud-Token'=>'asdf'}).to_timeout
159
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_timeout
159
160
 
160
161
  flipper = Flipper.new(Flipper::Adapters::Memory.new)
161
162