flipper 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -1
  3. data/.github/workflows/examples.yml +1 -1
  4. data/README.md +2 -0
  5. data/docs/images/banner.jpg +0 -0
  6. data/lib/flipper/adapters/actor_limit.rb +28 -0
  7. data/lib/flipper/adapters/cache_base.rb +143 -0
  8. data/lib/flipper/adapters/operation_logger.rb +18 -88
  9. data/lib/flipper/adapters/read_only.rb +6 -39
  10. data/lib/flipper/adapters/strict.rb +5 -10
  11. data/lib/flipper/adapters/wrapper.rb +54 -0
  12. data/lib/flipper/cli.rb +36 -17
  13. data/lib/flipper/cloud/configuration.rb +2 -3
  14. data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
  15. data/lib/flipper/cloud/telemetry.rb +10 -2
  16. data/lib/flipper/engine.rb +5 -5
  17. data/lib/flipper/poller.rb +6 -5
  18. data/lib/flipper/serializers/gzip.rb +3 -5
  19. data/lib/flipper/serializers/json.rb +3 -5
  20. data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
  21. data/lib/flipper/test/shared_adapter_test.rb +17 -17
  22. data/lib/flipper/typecast.rb +3 -3
  23. data/lib/flipper/version.rb +1 -1
  24. data/lib/flipper.rb +3 -1
  25. data/package-lock.json +41 -0
  26. data/package.json +10 -0
  27. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  28. data/spec/flipper/adapters/http_spec.rb +11 -2
  29. data/spec/flipper/cli_spec.rb +21 -46
  30. data/spec/flipper/cloud/configuration_spec.rb +2 -1
  31. data/spec/flipper/cloud/telemetry_spec.rb +52 -0
  32. data/spec/flipper/cloud_spec.rb +4 -2
  33. data/spec/flipper/engine_spec.rb +34 -4
  34. data/spec/flipper/middleware/memoizer_spec.rb +7 -4
  35. data/spec/support/fail_on_output.rb +8 -0
  36. data/spec/support/spec_helpers.rb +2 -1
  37. data/test/adapters/actor_limit_test.rb +20 -0
  38. data/test_rails/system/test_help_test.rb +1 -1
  39. metadata +14 -3
@@ -3,9 +3,11 @@ require "delegate"
3
3
  module Flipper
4
4
  module Cloud
5
5
  class Telemetry
6
- class Instrumenter < SimpleDelegator
6
+ class Instrumenter
7
+ attr_reader :instrumenter
8
+
7
9
  def initialize(cloud_configuration, instrumenter)
8
- super instrumenter
10
+ @instrumenter = instrumenter
9
11
  @cloud_configuration = cloud_configuration
10
12
  end
11
13
 
@@ -14,12 +16,6 @@ module Flipper
14
16
  @cloud_configuration.telemetry.record(name, payload)
15
17
  return_value
16
18
  end
17
-
18
- private
19
-
20
- def instrumenter
21
- __getobj__
22
- end
23
19
  end
24
20
  end
25
21
  end
@@ -160,8 +160,16 @@ module Flipper
160
160
  # thus may have a telemetry-interval header for us to respect.
161
161
  response ||= error.response if error && error.respond_to?(:response)
162
162
 
163
- if response && interval = response["telemetry-interval"]
164
- self.interval = interval.to_f
163
+ if response
164
+ if Flipper::Typecast.to_boolean(response["telemetry-shutdown"])
165
+ debug "action=telemetry_shutdown message=The server has requested that telemetry be shut down."
166
+ stop
167
+ return
168
+ end
169
+
170
+ if interval = response["telemetry-interval"]
171
+ self.interval = interval.to_f
172
+ end
165
173
  end
166
174
  rescue => error
167
175
  error "action=post_to_cloud error=#{error.inspect}"
@@ -25,6 +25,7 @@ module Flipper
25
25
  log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
26
26
  cloud_path: "_flipper",
27
27
  strict: default_strict_value,
28
+ actor_limit: ENV["FLIPPER_ACTOR_LIMIT"]&.to_i || 100,
28
29
  test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
29
30
  )
30
31
  end
@@ -65,13 +66,12 @@ module Flipper
65
66
  end
66
67
  end
67
68
 
68
- initializer "flipper.strict", after: :load_config_initializers do |app|
69
+ initializer "flipper.adapters", after: :load_config_initializers do |app|
69
70
  flipper = app.config.flipper
70
71
 
71
- if flipper.strict
72
- Flipper.configure do |config|
73
- config.use Flipper::Adapters::Strict, flipper.strict
74
- end
72
+ Flipper.configure do |config|
73
+ config.use Flipper::Adapters::Strict, flipper.strict if flipper.strict
74
+ config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
75
75
  end
76
76
  end
77
77
 
@@ -20,6 +20,8 @@ module Flipper
20
20
  instances.each {|_, instance| instance.stop }.clear
21
21
  end
22
22
 
23
+ MINIMUM_POLL_INTERVAL = 10
24
+
23
25
  def initialize(options = {})
24
26
  @thread = nil
25
27
  @pid = Process.pid
@@ -30,9 +32,9 @@ module Flipper
30
32
  @last_synced_at = Concurrent::AtomicFixnum.new(0)
31
33
  @adapter = Adapters::Memory.new(nil, threadsafe: true)
32
34
 
33
- if @interval < 1
34
- warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
35
- @interval = 1
35
+ if @interval < MINIMUM_POLL_INTERVAL
36
+ warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{@interval}. Setting @interval to #{MINIMUM_POLL_INTERVAL}."
37
+ @interval = MINIMUM_POLL_INTERVAL
36
38
  end
37
39
 
38
40
  @start_automatically = options.fetch(:start_automatically, true)
@@ -64,8 +66,7 @@ module Flipper
64
66
  # you can instrument these using poller.flipper
65
67
  end
66
68
 
67
- sleep_interval = interval - (Concurrent.monotonic_time - start)
68
- sleep sleep_interval if sleep_interval.positive?
69
+ sleep interval
69
70
  end
70
71
  end
71
72
 
@@ -3,10 +3,8 @@ require "stringio"
3
3
 
4
4
  module Flipper
5
5
  module Serializers
6
- module Gzip
7
- module_function
8
-
9
- def serialize(source)
6
+ class Gzip
7
+ def self.serialize(source)
10
8
  return if source.nil?
11
9
  output = StringIO.new
12
10
  gz = Zlib::GzipWriter.new(output)
@@ -15,7 +13,7 @@ module Flipper
15
13
  output.string
16
14
  end
17
15
 
18
- def deserialize(source)
16
+ def self.deserialize(source)
19
17
  return if source.nil?
20
18
  Zlib::GzipReader.wrap(StringIO.new(source), &:read)
21
19
  end
@@ -2,15 +2,13 @@ require "json"
2
2
 
3
3
  module Flipper
4
4
  module Serializers
5
- module Json
6
- module_function
7
-
8
- def serialize(source)
5
+ class Json
6
+ def self.serialize(source)
9
7
  return if source.nil?
10
8
  JSON.generate(source)
11
9
  end
12
10
 
13
- def deserialize(source)
11
+ def self.deserialize(source)
14
12
  return if source.nil?
15
13
  JSON.parse(source)
16
14
  end
@@ -108,19 +108,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
108
108
  actor22 = Flipper::Actor.new('22')
109
109
  actor_asdf = Flipper::Actor.new('asdf')
110
110
 
111
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
112
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true)
111
+ expect(feature.enable(actor22)).to be(true)
112
+ expect(feature.enable(actor_asdf)).to be(true)
113
113
 
114
- result = subject.get(feature)
115
- expect(result[:actors]).to eq(Set['22', 'asdf'])
114
+ expect(feature).to be_enabled(actor22)
115
+ expect(feature).to be_enabled(actor_asdf)
116
116
 
117
- expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
118
- result = subject.get(feature)
119
- expect(result[:actors]).to eq(Set['asdf'])
117
+ expect(feature.disable(actor22)).to be(true)
118
+ expect(feature).not_to be_enabled(actor22)
119
+ expect(feature).to be_enabled(actor_asdf)
120
120
 
121
- expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true)
122
- result = subject.get(feature)
123
- expect(result[:actors]).to eq(Set.new)
121
+ expect(feature.disable(actor_asdf)).to eq(true)
122
+ expect(feature).not_to be_enabled(actor22)
123
+ expect(feature).not_to be_enabled(actor_asdf)
124
124
  end
125
125
 
126
126
  it 'can enable, disable and get value for percentage of actors gate' do
@@ -182,9 +182,10 @@ RSpec.shared_examples_for 'a flipper adapter' do
182
182
  end
183
183
 
184
184
  it 'converts the actor value to a string' do
185
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))).to eq(true)
186
- result = subject.get(feature)
187
- expect(result[:actors]).to eq(Set['22'])
185
+ actor = Flipper::Actor.new(22)
186
+ expect(feature).not_to be_enabled(actor)
187
+ feature.enable_actor actor
188
+ expect(feature).to be_enabled(actor)
188
189
  end
189
190
 
190
191
  it 'converts group value to a string' do
@@ -295,9 +296,9 @@ RSpec.shared_examples_for 'a flipper adapter' do
295
296
 
296
297
  it 'can double enable an actor without error' do
297
298
  actor = Flipper::Actor.new('Flipper::Actor;22')
298
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true)
299
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true)
300
- expect(subject.get(feature).fetch(:actors)).to eq(Set['Flipper::Actor;22'])
299
+ expect(feature.enable(actor)).to be(true)
300
+ expect(feature.enable(actor)).to be(true)
301
+ expect(feature).to be_enabled(actor)
301
302
  end
302
303
 
303
304
  it 'can double enable a group without error' do
@@ -104,19 +104,19 @@ module Flipper
104
104
  actor22 = Flipper::Actor.new('22')
105
105
  actor_asdf = Flipper::Actor.new('asdf')
106
106
 
107
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
108
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
107
+ assert_equal true, @feature.enable(actor22)
108
+ assert_equal true, @feature.enable(actor_asdf)
109
109
 
110
- result = @adapter.get(@feature)
111
- assert_equal Set['22', 'asdf'], result[:actors]
110
+ assert @feature.enabled?(actor22)
111
+ assert @feature.enabled?(actor_asdf)
112
112
 
113
- assert true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
114
- result = @adapter.get(@feature)
115
- assert_equal Set['asdf'], result[:actors]
113
+ assert_equal true, @feature.disable(actor22)
114
+ refute @feature.enabled?(actor22)
115
+ assert @feature.enabled?(actor_asdf)
116
116
 
117
- assert_equal true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
118
- result = @adapter.get(@feature)
119
- assert_equal Set.new, result[:actors]
117
+ assert_equal true, @feature.disable(actor_asdf)
118
+ refute @feature.enabled?(actor22)
119
+ refute @feature.enabled?(actor_asdf)
120
120
  end
121
121
 
122
122
  def test_can_enable_disable_get_value_for_percentage_of_actors_gate
@@ -178,10 +178,10 @@ module Flipper
178
178
  end
179
179
 
180
180
  def test_converts_the_actor_value_to_a_string
181
- assert_equal true,
182
- @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))
183
- result = @adapter.get(@feature)
184
- assert_equal Set['22'], result[:actors]
181
+ actor = Flipper::Actor.new(22)
182
+ refute @feature.enabled?(actor)
183
+ @feature.enable_actor actor
184
+ assert @feature.enabled?(actor)
185
185
  end
186
186
 
187
187
  def test_converts_group_value_to_a_string
@@ -292,9 +292,9 @@ module Flipper
292
292
 
293
293
  def test_can_double_enable_an_actor_without_error
294
294
  actor = Flipper::Actor.new('Flipper::Actor;22')
295
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
296
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
297
- assert_equal Set['Flipper::Actor;22'], @adapter.get(@feature).fetch(:actors)
295
+ assert_equal true, @feature.enable(actor)
296
+ assert_equal true, @feature.enable(actor)
297
+ assert @feature.enabled?(actor)
298
298
  end
299
299
 
300
300
  def test_can_double_enable_a_group_without_error
@@ -3,8 +3,8 @@ require "flipper/serializers/json"
3
3
  require "flipper/serializers/gzip"
4
4
 
5
5
  module Flipper
6
- module Typecast
7
- TruthMap = {
6
+ class Typecast
7
+ TRUTH_MAP = {
8
8
  true => true,
9
9
  1 => true,
10
10
  'true' => true,
@@ -15,7 +15,7 @@ module Flipper
15
15
  #
16
16
  # Returns true or false.
17
17
  def self.to_boolean(value)
18
- !!TruthMap[value]
18
+ !!TRUTH_MAP[value]
19
19
  end
20
20
 
21
21
  # Internal: Convert value to an integer.
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
- VERSION = '1.2.2'.freeze
2
+ VERSION = '1.3.0'.freeze
3
3
 
4
4
  REQUIRED_RUBY_VERSION = '2.6'.freeze
5
5
  NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
data/lib/flipper.rb CHANGED
@@ -174,10 +174,12 @@ end
174
174
 
175
175
  require 'flipper/actor'
176
176
  require 'flipper/adapter'
177
+ require 'flipper/adapters/wrapper'
178
+ require 'flipper/adapters/actor_limit'
179
+ require 'flipper/adapters/instrumented'
177
180
  require 'flipper/adapters/memoizable'
178
181
  require 'flipper/adapters/memory'
179
182
  require 'flipper/adapters/strict'
180
- require 'flipper/adapters/instrumented'
181
183
  require 'flipper/adapter_builder'
182
184
  require 'flipper/configuration'
183
185
  require 'flipper/dsl'
data/package-lock.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "flipper",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "hasInstallScript": true,
8
+ "dependencies": {
9
+ "@popperjs/core": "^2.11.8",
10
+ "bootstrap": "^5.3.3"
11
+ }
12
+ },
13
+ "node_modules/@popperjs/core": {
14
+ "version": "2.11.8",
15
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
16
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
17
+ "funding": {
18
+ "type": "opencollective",
19
+ "url": "https://opencollective.com/popperjs"
20
+ }
21
+ },
22
+ "node_modules/bootstrap": {
23
+ "version": "5.3.3",
24
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
25
+ "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
26
+ "funding": [
27
+ {
28
+ "type": "github",
29
+ "url": "https://github.com/sponsors/twbs"
30
+ },
31
+ {
32
+ "type": "opencollective",
33
+ "url": "https://opencollective.com/bootstrap"
34
+ }
35
+ ],
36
+ "peerDependencies": {
37
+ "@popperjs/core": "^2.11.8"
38
+ }
39
+ }
40
+ }
41
+ }
data/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@popperjs/core": "^2.11.8",
5
+ "bootstrap": "^5.3.3"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "script/vendor-assets"
9
+ }
10
+ }
@@ -0,0 +1,20 @@
1
+ require "flipper/adapters/actor_limit"
2
+
3
+ RSpec.describe Flipper::Adapters::ActorLimit do
4
+ it_should_behave_like 'a flipper adapter' do
5
+ let(:limit) { 5 }
6
+ let(:adapter) { Flipper::Adapters::ActorLimit.new(Flipper::Adapters::Memory.new, limit) }
7
+
8
+ subject { adapter }
9
+
10
+ describe '#enable' do
11
+ it "fails when limit exceeded" do
12
+ 5.times { |i| feature.enable Flipper::Actor.new("User;#{i}") }
13
+
14
+ expect {
15
+ feature.enable Flipper::Actor.new("User;6")
16
+ }.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,6 +1,15 @@
1
1
  require 'flipper/adapters/http'
2
2
  require 'flipper/adapters/pstore'
3
- require 'rack/handler/webrick'
3
+
4
+ rack_handler = begin
5
+ # Rack 3+
6
+ require 'rackup/handler/webrick'
7
+ Rackup::Handler::WEBrick
8
+ rescue LoadError
9
+ require 'rack/handler/webrick'
10
+ Rack::Handler::WEBrick
11
+ end
12
+
4
13
 
5
14
  FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
6
15
 
@@ -36,7 +45,7 @@ RSpec.describe Flipper::Adapters::Http do
36
45
  ],
37
46
  }
38
47
  @server = WEBrick::HTTPServer.new(server_options)
39
- @server.mount '/', Rack::Handler::WEBrick, app
48
+ @server.mount '/', rack_handler, app
40
49
 
41
50
  Thread.new { @server.start }
42
51
  Timeout.timeout(1) { :wait until @started }
@@ -1,13 +1,33 @@
1
1
  require "flipper/cli"
2
2
 
3
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
+
4
14
  # Infer the command from the description
5
15
  subject(:argv) do
6
16
  descriptions = self.class.parent_groups.map {|g| g.metadata[:description_args] }.reverse.flatten.drop(1)
7
17
  descriptions.map { |arg| Shellwords.split(arg) }.flatten
8
18
  end
9
19
 
10
- subject { run argv }
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
11
31
 
12
32
  before do
13
33
  ENV["FLIPPER_REQUIRE"] = "./spec/fixtures/environment"
@@ -141,49 +161,4 @@ RSpec.describe Flipper::CLI do
141
161
  it { should have_attributes(status: 0, stdout: /enabled.*admins/m) }
142
162
  end
143
163
  end
144
-
145
- context "bundler is not installed" do
146
- let(:argv) { "list" }
147
-
148
- around do |example|
149
- original_bundler = Bundler
150
- begin
151
- Object.send(:remove_const, :Bundler)
152
- example.run
153
- ensure
154
- Object.const_set(:Bundler, original_bundler)
155
- end
156
- end
157
-
158
- it "should not raise an error" do
159
- Flipper.enable(:enabled_feature)
160
- Flipper.enable_group(:enabled_groups, :admins)
161
- Flipper.add(:disabled_feature)
162
-
163
- expect(subject).to have_attributes(status: 0, stdout: /enabled_feature.*enabled_groups.*disabled_feature/m)
164
- end
165
- end
166
-
167
- def run(argv)
168
- original_stdout = $stdout
169
- original_stderr = $stderr
170
-
171
- $stdout = StringIO.new
172
- $stderr = StringIO.new
173
- status = 0
174
-
175
- # Prentend this a TTY so we can test colorization
176
- allow($stdout).to receive(:tty?).and_return(true)
177
-
178
- begin
179
- Flipper::CLI.run(argv)
180
- rescue SystemExit => e
181
- status = e.status
182
- end
183
-
184
- OpenStruct.new(status: status, stdout: $stdout.string, stderr: $stderr.string)
185
- ensure
186
- $stdout = original_stdout
187
- $stderr = original_stderr
188
- end
189
164
  end
@@ -20,7 +20,8 @@ RSpec.describe Flipper::Cloud::Configuration do
20
20
  it "can set instrumenter" do
21
21
  instrumenter = Object.new
22
22
  instance = described_class.new(required_options.merge(instrumenter: instrumenter))
23
- 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)
24
25
  end
25
26
 
26
27
  it "can set read_timeout" do
@@ -2,6 +2,12 @@ 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
13
  to_return(status: 200, body: "{}")
@@ -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"})
@@ -36,7 +36,8 @@ RSpec.describe Flipper::Cloud do
36
36
  expect(client.uri.host).to eq('www.flippercloud.io')
37
37
  expect(client.uri.path).to eq('/adapter')
38
38
  expect(client.headers["flipper-cloud-token"]).to eq(token)
39
- expect(@instance.instrumenter).to be(Flipper::Instrumenters::Noop)
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
 
@@ -62,7 +63,8 @@ RSpec.describe Flipper::Cloud do
62
63
  it 'can set instrumenter' do
63
64
  instrumenter = Flipper::Instrumenters::Memory.new
64
65
  instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
65
- 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)
66
68
  end
67
69
 
68
70
  it 'allows wrapping adapter with another adapter like the instrumenter' do