flipper 1.2.1 → 1.3.0.pre

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 (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