flipper 1.2.1 → 1.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) 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/Gemfile +0 -1
  5. data/README.md +1 -0
  6. data/lib/flipper/adapters/cache_base.rb +143 -0
  7. data/lib/flipper/adapters/operation_logger.rb +18 -88
  8. data/lib/flipper/adapters/read_only.rb +6 -39
  9. data/lib/flipper/adapters/strict.rb +5 -10
  10. data/lib/flipper/adapters/wrapper.rb +54 -0
  11. data/lib/flipper/cli.rb +38 -15
  12. data/lib/flipper/cloud/configuration.rb +2 -3
  13. data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
  14. data/lib/flipper/cloud/telemetry.rb +10 -2
  15. data/lib/flipper/poller.rb +6 -5
  16. data/lib/flipper/serializers/gzip.rb +3 -5
  17. data/lib/flipper/serializers/json.rb +3 -5
  18. data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
  19. data/lib/flipper/test/shared_adapter_test.rb +17 -17
  20. data/lib/flipper/test_help.rb +15 -10
  21. data/lib/flipper/typecast.rb +3 -3
  22. data/lib/flipper/version.rb +1 -1
  23. data/lib/flipper.rb +1 -0
  24. data/package-lock.json +41 -0
  25. data/package.json +10 -0
  26. data/spec/flipper/adapters/http_spec.rb +11 -2
  27. data/spec/flipper/cli_spec.rb +23 -23
  28. data/spec/flipper/cloud/configuration_spec.rb +29 -37
  29. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
  30. data/spec/flipper/cloud/telemetry_spec.rb +52 -0
  31. data/spec/flipper/cloud_spec.rb +6 -5
  32. data/spec/flipper/engine_spec.rb +39 -49
  33. data/spec/flipper/middleware/memoizer_spec.rb +7 -4
  34. data/spec/support/fail_on_output.rb +8 -0
  35. data/spec/support/spec_helpers.rb +2 -1
  36. data/test_rails/system/test_help_test.rb +8 -3
  37. metadata +9 -5
  38. data/spec/support/climate_control.rb +0 -7
@@ -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
 
@@ -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
@@ -4,8 +4,10 @@ require 'flipper/engine'
4
4
  RSpec.describe Flipper::Engine do
5
5
  let(:application) do
6
6
  Class.new(Rails::Application) do
7
+ config.load_defaults Rails::VERSION::STRING.to_f
7
8
  config.eager_load = false
8
9
  config.logger = ActiveSupport::Logger.new($stdout)
10
+ config.active_support.remove_deprecated_time_with_zone_name = false
9
11
  end.instance
10
12
  end
11
13
 
@@ -33,28 +35,25 @@ RSpec.describe Flipper::Engine do
33
35
  let(:adapter) { Flipper.adapter.adapter }
34
36
 
35
37
  it 'can set strict=true from ENV' do
36
- with_env 'FLIPPER_STRICT' => 'true' do
37
- subject
38
- expect(config.strict).to eq(:raise)
39
- expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
40
- end
38
+ ENV['FLIPPER_STRICT'] = 'true'
39
+ subject
40
+ expect(config.strict).to eq(:raise)
41
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
41
42
  end
42
43
 
43
44
  it 'can set strict=warn from ENV' do
44
- with_env 'FLIPPER_STRICT' => 'warn' do
45
- subject
46
- expect(config.strict).to eq(:warn)
47
- expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
48
- expect(adapter.handler).to be(:warn)
49
- end
45
+ ENV['FLIPPER_STRICT'] = 'warn'
46
+ subject
47
+ expect(config.strict).to eq(:warn)
48
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
49
+ expect(adapter.handler).to be(:warn)
50
50
  end
51
51
 
52
52
  it 'can set strict=false from ENV' do
53
- with_env 'FLIPPER_STRICT' => 'false' do
54
- subject
55
- expect(config.strict).to eq(false)
56
- expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
57
- end
53
+ ENV['FLIPPER_STRICT'] = 'false'
54
+ subject
55
+ expect(config.strict).to eq(false)
56
+ expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
58
57
  end
59
58
 
60
59
  [true, :raise, :warn].each do |value|
@@ -104,39 +103,34 @@ RSpec.describe Flipper::Engine do
104
103
  it_behaves_like 'config.strict'
105
104
 
106
105
  it 'can set env_key from ENV' do
107
- with_env 'FLIPPER_ENV_KEY' => 'flopper' do
108
- subject
109
- expect(config.env_key).to eq('flopper')
110
- end
106
+ ENV['FLIPPER_ENV_KEY'] = 'flopper'
107
+ subject
108
+ expect(config.env_key).to eq('flopper')
111
109
  end
112
110
 
113
111
  it 'can set memoize from ENV' do
114
- with_env 'FLIPPER_MEMOIZE' => 'false' do
115
- subject
116
- expect(config.memoize).to eq(false)
117
- end
112
+ ENV['FLIPPER_MEMOIZE'] = 'false'
113
+ subject
114
+ expect(config.memoize).to eq(false)
118
115
  end
119
116
 
120
117
  it 'can set preload from ENV' do
121
- with_env 'FLIPPER_PRELOAD' => 'false' do
122
- subject
123
- expect(config.preload).to eq(false)
124
- end
118
+ ENV['FLIPPER_PRELOAD'] = 'false'
119
+ subject
120
+ expect(config.preload).to eq(false)
125
121
  end
126
122
 
127
123
  it 'can set instrumenter from ENV' do
128
124
  stub_const('My::Cool::Instrumenter', Class.new)
129
- with_env 'FLIPPER_INSTRUMENTER' => 'My::Cool::Instrumenter' do
130
- subject
131
- expect(config.instrumenter).to eq(My::Cool::Instrumenter)
132
- end
125
+ ENV['FLIPPER_INSTRUMENTER'] = 'My::Cool::Instrumenter'
126
+ subject
127
+ expect(config.instrumenter).to eq(My::Cool::Instrumenter)
133
128
  end
134
129
 
135
130
  it 'can set log from ENV' do
136
- with_env 'FLIPPER_LOG' => 'false' do
137
- subject
138
- expect(config.log).to eq(false)
139
- end
131
+ ENV['FLIPPER_LOG'] = 'false'
132
+ subject
133
+ expect(config.log).to eq(false)
140
134
  end
141
135
 
142
136
  it 'sets defaults' do
@@ -220,10 +214,8 @@ RSpec.describe Flipper::Engine do
220
214
  end
221
215
 
222
216
  context 'with cloud' do
223
- around do |example|
224
- with_env "FLIPPER_CLOUD_TOKEN" => "test-token" do
225
- example.run
226
- end
217
+ before do
218
+ ENV["FLIPPER_CLOUD_TOKEN"] = "test-token"
227
219
  end
228
220
 
229
221
  # App for Rack::Test
@@ -244,14 +236,13 @@ RSpec.describe Flipper::Engine do
244
236
  application.initialize!
245
237
 
246
238
  expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
247
- expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
239
+ expect(Flipper.instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
240
+ expect(Flipper.instance.instrumenter.instrumenter).to be(ActiveSupport::Notifications)
248
241
  end
249
242
 
250
243
  context "with CLOUD_SYNC_SECRET" do
251
- around do |example|
252
- with_env "FLIPPER_CLOUD_SYNC_SECRET" => "test-secret" do
253
- example.run
254
- end
244
+ before do
245
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "test-secret"
255
246
  end
256
247
 
257
248
  let(:request_body) do
@@ -296,10 +287,9 @@ RSpec.describe Flipper::Engine do
296
287
 
297
288
  context "without FLIPPER_CLOUD_TOKEN" do
298
289
  it "gracefully skips configuring webhook app" do
299
- with_env "FLIPPER_CLOUD_TOKEN" => nil do
300
- application.initialize!
301
- expect(Flipper.instance).to be_a(Flipper::DSL)
302
- end
290
+ ENV["FLIPPER_CLOUD_TOKEN"] = nil
291
+ application.initialize!
292
+ expect(Flipper.instance).to be_a(Flipper::DSL)
303
293
 
304
294
  post "/_flipper"
305
295
  expect(last_response.status).to eq(404)
@@ -458,7 +458,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
458
458
  logged_memory = Flipper::Adapters::OperationLogger.new(memory)
459
459
  cache = ActiveSupport::Cache::MemoryStore.new
460
460
  cache.clear
461
- cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
461
+ cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache)
462
462
  logged_cached = Flipper::Adapters::OperationLogger.new(cached)
463
463
  memo = {}
464
464
  flipper = Flipper.new(logged_cached)
@@ -471,15 +471,18 @@ RSpec.describe Flipper::Middleware::Memoizer do
471
471
 
472
472
  get '/', {}, 'flipper' => flipper
473
473
  expect(logged_cached.count(:get_all)).to be(1)
474
- expect(logged_memory.count(:get_all)).to be(1)
474
+ expect(logged_memory.count(:features)).to be(1)
475
+ expect(logged_memory.count(:get_multi)).to be(1)
475
476
 
476
477
  get '/', {}, 'flipper' => flipper
477
478
  expect(logged_cached.count(:get_all)).to be(2)
478
- expect(logged_memory.count(:get_all)).to be(1)
479
+ expect(logged_memory.count(:features)).to be(1)
480
+ expect(logged_memory.count(:get_multi)).to be(1)
479
481
 
480
482
  get '/', {}, 'flipper' => flipper
481
483
  expect(logged_cached.count(:get_all)).to be(3)
482
- expect(logged_memory.count(:get_all)).to be(1)
484
+ expect(logged_memory.count(:features)).to be(1)
485
+ expect(logged_memory.count(:get_multi)).to be(1)
483
486
  end
484
487
  end
485
488
  end
@@ -0,0 +1,8 @@
1
+ if ENV["CI"] || ENV["FAIL_ON_OUTPUT"]
2
+ RSpec.configure do |config|
3
+ config.around do |example|
4
+ output = silence { example.run }
5
+ fail "Use `silence { }` to avoid printing to STDOUT/STDERR\n#{output}" unless output.empty?
6
+ end
7
+ end
8
+ end
@@ -1,6 +1,7 @@
1
1
  require 'ice_age'
2
2
  require 'json'
3
3
  require 'rack/test'
4
+ require 'rack/session'
4
5
 
5
6
  module SpecHelpers
6
7
  extend self
@@ -12,7 +13,7 @@ module SpecHelpers
12
13
 
13
14
  def build_app(flipper, options = {})
14
15
  Flipper::UI.app(flipper, options) do |builder|
15
- builder.use Rack::Session::Cookie, secret: 'test'
16
+ builder.use Rack::Session::Cookie, secret: 'test' * 16 # Rack 3+ wants a 64-character secret
16
17
  end
17
18
  end
18
19
 
@@ -29,10 +29,15 @@ end
29
29
 
30
30
  class TestHelpTest < ActionDispatch::SystemTestCase
31
31
  # Any driver that runs the app in a separate thread will test what we want here.
32
- driven_by :cuprite
32
+ driven_by :cuprite, options: { process_timeout: 60 }
33
33
 
34
- # Ensure this test uses this app instance
35
- setup { Rails.application = TestApp.instance }
34
+ setup do
35
+ # Reconfigure Flipper since other tests change the adapter.
36
+ flipper_configure
37
+
38
+ # Ensure this test uses this app instance
39
+ Rails.application = TestApp.instance
40
+ end
36
41
 
37
42
  test "configures a shared adapter between tests and app" do
38
43
  Flipper.disable(:test)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-15 00:00:00.000000000 Z
11
+ date: 2024-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -90,6 +90,7 @@ files:
90
90
  - lib/flipper/actor.rb
91
91
  - lib/flipper/adapter.rb
92
92
  - lib/flipper/adapter_builder.rb
93
+ - lib/flipper/adapters/cache_base.rb
93
94
  - lib/flipper/adapters/dual_write.rb
94
95
  - lib/flipper/adapters/failover.rb
95
96
  - lib/flipper/adapters/failsafe.rb
@@ -109,6 +110,7 @@ files:
109
110
  - lib/flipper/adapters/sync/feature_synchronizer.rb
110
111
  - lib/flipper/adapters/sync/interval_synchronizer.rb
111
112
  - lib/flipper/adapters/sync/synchronizer.rb
113
+ - lib/flipper/adapters/wrapper.rb
112
114
  - lib/flipper/cli.rb
113
115
  - lib/flipper/cloud.rb
114
116
  - lib/flipper/cloud/configuration.rb
@@ -193,6 +195,8 @@ files:
193
195
  - lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb
194
196
  - lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb
195
197
  - lib/generators/flipper/update_generator.rb
198
+ - package-lock.json
199
+ - package.json
196
200
  - spec/fixtures/environment.rb
197
201
  - spec/fixtures/feature.json
198
202
  - spec/fixtures/flipper_pstore_1679087600.json
@@ -286,8 +290,8 @@ files:
286
290
  - spec/flipper_spec.rb
287
291
  - spec/spec_helper.rb
288
292
  - spec/support/actor_names.yml
289
- - spec/support/climate_control.rb
290
293
  - spec/support/descriptions.yml
294
+ - spec/support/fail_on_output.rb
291
295
  - spec/support/fake_backoff_policy.rb
292
296
  - spec/support/fake_udp_socket.rb
293
297
  - spec/support/skippable.rb
@@ -307,7 +311,7 @@ metadata:
307
311
  homepage_uri: https://www.flippercloud.io
308
312
  source_code_uri: https://github.com/flippercloud/flipper
309
313
  bug_tracker_uri: https://github.com/flippercloud/flipper/issues
310
- changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.2.1
314
+ changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.0.pre
311
315
  post_install_message:
312
316
  rdoc_options: []
313
317
  require_paths:
@@ -421,8 +425,8 @@ test_files:
421
425
  - spec/flipper_spec.rb
422
426
  - spec/spec_helper.rb
423
427
  - spec/support/actor_names.yml
424
- - spec/support/climate_control.rb
425
428
  - spec/support/descriptions.yml
429
+ - spec/support/fail_on_output.rb
426
430
  - spec/support/fake_backoff_policy.rb
427
431
  - spec/support/fake_udp_socket.rb
428
432
  - spec/support/skippable.rb
@@ -1,7 +0,0 @@
1
- require 'climate_control'
2
-
3
- RSpec.configure do |config|
4
- def with_env(options = {}, &block)
5
- ClimateControl.modify(options, &block)
6
- end
7
- end