flipper 0.22.0 → 0.28.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/.codeclimate.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +26 -20
- data/.github/workflows/examples.yml +62 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Changelog.md +152 -3
- data/Dockerfile +1 -1
- data/Gemfile +9 -6
- data/README.md +15 -67
- data/Rakefile +4 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/docker-compose.yml +37 -34
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/dsl.rb +3 -3
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/flipper.gemspec +2 -2
- data/lib/flipper/actor.rb +4 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/dual_write.rb +10 -16
- data/lib/flipper/adapters/failover.rb +83 -0
- data/lib/flipper/adapters/failsafe.rb +76 -0
- data/lib/flipper/adapters/http/client.rb +18 -12
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +14 -4
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +27 -18
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +39 -0
- data/lib/flipper/adapters/pstore.rb +2 -5
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/adapters/sync.rb +9 -15
- data/lib/flipper/dsl.rb +9 -11
- data/lib/flipper/errors.rb +3 -20
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/feature.rb +32 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +7 -3
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +2 -12
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/railtie.rb +23 -22
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +6 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/actor_spec.rb +10 -2
- data/spec/flipper/adapter_spec.rb +29 -4
- data/spec/flipper/adapters/dual_write_spec.rb +0 -2
- data/spec/flipper/adapters/failover_spec.rb +129 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http_spec.rb +64 -6
- data/spec/flipper/adapters/instrumented_spec.rb +28 -12
- data/spec/flipper/adapters/memoizable_spec.rb +30 -12
- data/spec/flipper/adapters/memory_spec.rb +3 -4
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +0 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync_spec.rb +0 -2
- data/spec/flipper/configuration_spec.rb +0 -1
- data/spec/flipper/dsl_spec.rb +38 -12
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -19
- data/spec/flipper/feature_spec.rb +76 -33
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +0 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +0 -23
- data/spec/flipper/middleware/setup_env_spec.rb +0 -2
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/railtie_spec.rb +73 -33
- data/spec/flipper/registry_spec.rb +0 -1
- data/spec/flipper/typecast_spec.rb +82 -4
- data/spec/flipper/types/actor_spec.rb +45 -46
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +2 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/flipper_integration_spec.rb +62 -51
- data/spec/flipper_spec.rb +7 -2
- data/spec/{helper.rb → spec_helper.rb} +4 -2
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +2 -6
- metadata +63 -19
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -137
- data/docs/api/README.md +0 -884
- data/docs/http/README.md +0 -36
- data/docs/read-only/README.md +0 -24
|
@@ -34,7 +34,16 @@ module Flipper
|
|
|
34
34
|
assert_includes @adapter.class.ancestors, Flipper::Adapter
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def test_knows_how_to_get_adapter_from_source
|
|
38
|
+
adapter = Flipper::Adapters::Memory.new
|
|
39
|
+
flipper = Flipper.new(adapter)
|
|
40
|
+
|
|
41
|
+
assert_includes adapter.class.from(adapter).class.ancestors, Flipper::Adapter
|
|
42
|
+
assert_includes adapter.class.from(flipper).class.ancestors, Flipper::Adapter
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
def test_returns_correct_default_values_for_gates_if_none_are_enabled
|
|
46
|
+
assert_equal @adapter.class.default_config, @adapter.get(@feature)
|
|
38
47
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
|
39
48
|
end
|
|
40
49
|
|
|
@@ -300,6 +309,21 @@ module Flipper
|
|
|
300
309
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
|
|
301
310
|
assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
|
|
302
311
|
end
|
|
312
|
+
|
|
313
|
+
def test_can_import_and_export
|
|
314
|
+
adapter = Flipper::Adapters::Memory.new
|
|
315
|
+
source_flipper = Flipper.new(adapter)
|
|
316
|
+
source_flipper.enable(:stats)
|
|
317
|
+
export = adapter.export
|
|
318
|
+
|
|
319
|
+
# some adapters cannot import so if they return false lets assert it
|
|
320
|
+
# didn't happen
|
|
321
|
+
if @adapter.import(export)
|
|
322
|
+
assert @flipper[:stats].enabled?
|
|
323
|
+
else
|
|
324
|
+
refute @flipper[:stats].enabled?
|
|
325
|
+
end
|
|
326
|
+
end
|
|
303
327
|
end
|
|
304
328
|
end
|
|
305
329
|
end
|
data/lib/flipper/typecast.rb
CHANGED
|
@@ -21,11 +21,9 @@ module Flipper
|
|
|
21
21
|
# Returns an Integer representation of the value.
|
|
22
22
|
# Raises ArgumentError if conversion is not possible.
|
|
23
23
|
def self.to_integer(value)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
|
28
|
-
end
|
|
24
|
+
value.to_i
|
|
25
|
+
rescue NoMethodError
|
|
26
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
# Internal: Convert value to a float.
|
|
@@ -33,11 +31,9 @@ module Flipper
|
|
|
33
31
|
# Returns a Float representation of the value.
|
|
34
32
|
# Raises ArgumentError if conversion is not possible.
|
|
35
33
|
def self.to_float(value)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
|
40
|
-
end
|
|
34
|
+
value.to_f
|
|
35
|
+
rescue NoMethodError
|
|
36
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
# Internal: Convert value to a percentage.
|
|
@@ -45,11 +41,11 @@ module Flipper
|
|
|
45
41
|
# Returns a Integer or Float representation of the value.
|
|
46
42
|
# Raises ArgumentError if conversion is not possible.
|
|
47
43
|
def self.to_percentage(value)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
result_to_f = value.to_f
|
|
45
|
+
result_to_i = result_to_f.to_i
|
|
46
|
+
result_to_f == result_to_i ? result_to_i : result_to_f
|
|
47
|
+
rescue NoMethodError
|
|
48
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
|
|
53
49
|
end
|
|
54
50
|
|
|
55
51
|
# Internal: Convert value to a set.
|
|
@@ -66,5 +62,22 @@ module Flipper
|
|
|
66
62
|
raise ArgumentError, "#{value.inspect} cannot be converted to a set"
|
|
67
63
|
end
|
|
68
64
|
end
|
|
65
|
+
|
|
66
|
+
def self.features_hash(source)
|
|
67
|
+
normalized_source = {}
|
|
68
|
+
(source || {}).each do |feature_key, gates|
|
|
69
|
+
normalized_source[feature_key] ||= {}
|
|
70
|
+
gates.each do |gate_key, value|
|
|
71
|
+
normalized_value = case value
|
|
72
|
+
when Array, Set
|
|
73
|
+
value.to_set
|
|
74
|
+
else
|
|
75
|
+
value ? value.to_s : value
|
|
76
|
+
end
|
|
77
|
+
normalized_source[feature_key][gate_key.to_sym] = normalized_value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
normalized_source
|
|
81
|
+
end
|
|
69
82
|
end
|
|
70
83
|
end
|
data/lib/flipper/types/actor.rb
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
module Flipper
|
|
2
2
|
module Types
|
|
3
3
|
class Actor < Type
|
|
4
|
-
def self.wrappable?(
|
|
5
|
-
return false if
|
|
6
|
-
|
|
4
|
+
def self.wrappable?(actor)
|
|
5
|
+
return false if actor.nil?
|
|
6
|
+
actor.respond_to?(:flipper_id)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
attr_reader :
|
|
9
|
+
attr_reader :actor
|
|
10
10
|
|
|
11
|
-
def initialize(
|
|
12
|
-
raise ArgumentError, '
|
|
11
|
+
def initialize(actor)
|
|
12
|
+
raise ArgumentError, 'actor cannot be nil' if actor.nil?
|
|
13
13
|
|
|
14
|
-
unless
|
|
15
|
-
raise ArgumentError, "#{
|
|
14
|
+
unless actor.respond_to?(:flipper_id)
|
|
15
|
+
raise ArgumentError, "#{actor.inspect} must respond to flipper_id, but does not"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
@
|
|
19
|
-
@value =
|
|
18
|
+
@actor = actor
|
|
19
|
+
@value = actor.flipper_id.to_s
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def respond_to?(*args)
|
|
23
|
-
super || @
|
|
23
|
+
super || @actor.respond_to?(*args)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if RUBY_VERSION >= '3.0'
|
|
27
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
28
|
+
@actor.send name, *args, **kwargs, &block
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
def method_missing(name, *args, &block)
|
|
32
|
+
@actor.send name, *args, &block
|
|
33
|
+
end
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
end
|
data/lib/flipper/types/group.rb
CHANGED
|
@@ -14,20 +14,27 @@ module Flipper
|
|
|
14
14
|
|
|
15
15
|
if block_given?
|
|
16
16
|
@block = block
|
|
17
|
-
@single_argument = @block
|
|
17
|
+
@single_argument = call_with_no_context?(@block)
|
|
18
18
|
else
|
|
19
|
-
@block = ->(
|
|
19
|
+
@block = ->(actor, context) { false }
|
|
20
20
|
@single_argument = false
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def match?(
|
|
24
|
+
def match?(actor, context)
|
|
25
25
|
if @single_argument
|
|
26
|
-
@block.call(
|
|
26
|
+
@block.call(actor)
|
|
27
27
|
else
|
|
28
|
-
@block.call(
|
|
28
|
+
@block.call(actor, context)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
NO_PARAMS_IN_RUBY_3 = [[:req], [:rest]]
|
|
33
|
+
def call_with_no_context?(block)
|
|
34
|
+
return true if block.parameters == NO_PARAMS_IN_RUBY_3
|
|
35
|
+
|
|
36
|
+
block.arity.abs == 1
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
end
|
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
|
@@ -64,7 +64,7 @@ module Flipper
|
|
|
64
64
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
|
65
65
|
:time, :percentage_of_time,
|
|
66
66
|
:features, :feature, :[], :preload, :preload_all,
|
|
67
|
-
:adapter, :add, :exist?, :remove, :import,
|
|
67
|
+
:adapter, :add, :exist?, :remove, :import, :export,
|
|
68
68
|
:memoize=, :memoizing?,
|
|
69
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
|
70
70
|
|
|
@@ -72,12 +72,12 @@ module Flipper
|
|
|
72
72
|
#
|
|
73
73
|
# name - The Symbol name of the group.
|
|
74
74
|
# block - The block that should be used to determine if the group matches a
|
|
75
|
-
# given
|
|
75
|
+
# given actor.
|
|
76
76
|
#
|
|
77
77
|
# Examples
|
|
78
78
|
#
|
|
79
|
-
# Flipper.register(:admins) { |
|
|
80
|
-
#
|
|
79
|
+
# Flipper.register(:admins) { |actor|
|
|
80
|
+
# actor.respond_to?(:admin?) && actor.admin?
|
|
81
81
|
# }
|
|
82
82
|
#
|
|
83
83
|
# Returns a Flipper::Group.
|
|
@@ -155,6 +155,7 @@ require 'flipper/instrumenters/noop'
|
|
|
155
155
|
require 'flipper/identifier'
|
|
156
156
|
require 'flipper/middleware/memoizer'
|
|
157
157
|
require 'flipper/middleware/setup_env'
|
|
158
|
+
require 'flipper/poller'
|
|
158
159
|
require 'flipper/registry'
|
|
159
160
|
require 'flipper/type'
|
|
160
161
|
require 'flipper/types/actor'
|
|
@@ -164,5 +165,6 @@ require 'flipper/types/percentage'
|
|
|
164
165
|
require 'flipper/types/percentage_of_actors'
|
|
165
166
|
require 'flipper/types/percentage_of_time'
|
|
166
167
|
require 'flipper/typecast'
|
|
168
|
+
require 'flipper/version'
|
|
167
169
|
|
|
168
170
|
require "flipper/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"features": {
|
|
4
|
+
"search": {
|
|
5
|
+
"boolean": null,
|
|
6
|
+
"actors": [
|
|
7
|
+
"john",
|
|
8
|
+
"another",
|
|
9
|
+
"testing"
|
|
10
|
+
],
|
|
11
|
+
"percentage_of_actors": null,
|
|
12
|
+
"percentage_of_time": null,
|
|
13
|
+
"groups": [
|
|
14
|
+
"admins"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"new_pricing": {
|
|
18
|
+
"boolean": "true",
|
|
19
|
+
"actors": [],
|
|
20
|
+
"percentage_of_actors": null,
|
|
21
|
+
"percentage_of_time": null,
|
|
22
|
+
"groups": []
|
|
23
|
+
},
|
|
24
|
+
"google_analytics_tag": {
|
|
25
|
+
"boolean": null,
|
|
26
|
+
"actors": [],
|
|
27
|
+
"percentage_of_actors": "100",
|
|
28
|
+
"percentage_of_time": null,
|
|
29
|
+
"groups": []
|
|
30
|
+
},
|
|
31
|
+
"help_scout_tag": {
|
|
32
|
+
"boolean": null,
|
|
33
|
+
"actors": [],
|
|
34
|
+
"percentage_of_actors": null,
|
|
35
|
+
"percentage_of_time": "50",
|
|
36
|
+
"groups": []
|
|
37
|
+
},
|
|
38
|
+
"nope": {
|
|
39
|
+
"boolean": null,
|
|
40
|
+
"actors": [],
|
|
41
|
+
"percentage_of_actors": null,
|
|
42
|
+
"percentage_of_time": null,
|
|
43
|
+
"groups": []
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
data/spec/flipper/actor_spec.rb
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'helper'
|
|
2
|
-
|
|
3
1
|
RSpec.describe Flipper::Actor do
|
|
4
2
|
it 'initializes with and knows flipper_id' do
|
|
5
3
|
actor = described_class.new("User;235")
|
|
@@ -45,4 +43,14 @@ RSpec.describe Flipper::Actor do
|
|
|
45
43
|
expect(actor1.==(actor2)).to be(false)
|
|
46
44
|
end
|
|
47
45
|
end
|
|
46
|
+
|
|
47
|
+
describe '#hash' do
|
|
48
|
+
it 'returns a hash-value based on the flipper id' do
|
|
49
|
+
h = {
|
|
50
|
+
described_class.new("User;123") => true
|
|
51
|
+
}
|
|
52
|
+
expect(h).to have_key(described_class.new("User;123"))
|
|
53
|
+
expect(h).not_to have_key(described_class.new("User;456"))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
48
56
|
end
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require 'helper'
|
|
2
|
-
|
|
3
1
|
RSpec.describe Flipper::Adapter do
|
|
4
2
|
let(:source_flipper) { build_flipper }
|
|
5
3
|
let(:destination_flipper) { build_flipper }
|
|
@@ -32,9 +30,9 @@ RSpec.describe Flipper::Adapter do
|
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
describe '#import' do
|
|
35
|
-
it 'returns
|
|
33
|
+
it 'returns true' do
|
|
36
34
|
result = destination_flipper.import(source_flipper)
|
|
37
|
-
expect(result).to be(
|
|
35
|
+
expect(result).to be(true)
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
it 'can import from one adapter to another' do
|
|
@@ -116,5 +114,32 @@ RSpec.describe Flipper::Adapter do
|
|
|
116
114
|
destination_flipper.import(source_flipper)
|
|
117
115
|
expect(destination_flipper.features.map(&:key)).to eq([])
|
|
118
116
|
end
|
|
117
|
+
|
|
118
|
+
it 'can import an export' do
|
|
119
|
+
source_flipper.enable(:search)
|
|
120
|
+
source_flipper.enable(:google_analytics, Flipper::Actor.new("User;1"))
|
|
121
|
+
|
|
122
|
+
destination_flipper.import(source_flipper.export)
|
|
123
|
+
|
|
124
|
+
feature = destination_flipper[:search]
|
|
125
|
+
expect(feature.boolean_value).to be(true)
|
|
126
|
+
|
|
127
|
+
feature = destination_flipper[:google_analytics]
|
|
128
|
+
expect(feature.actors_value).to eq(Set["User;1"])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "#export" do
|
|
133
|
+
it "exports features" do
|
|
134
|
+
source_flipper.enable(:search)
|
|
135
|
+
export = source_flipper.export
|
|
136
|
+
expect(export.features.dig("search", :boolean)).to eq("true")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "exports with arguments" do
|
|
140
|
+
source_flipper.enable(:search)
|
|
141
|
+
export = source_flipper.export(format: :json, version: 1)
|
|
142
|
+
expect(export.features.dig("search", :boolean)).to eq("true")
|
|
143
|
+
end
|
|
119
144
|
end
|
|
120
145
|
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'dalli'
|
|
2
|
+
require 'flipper/adapters/failover'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'pstore'
|
|
5
|
+
require 'redis'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Flipper::Adapters::Failover do
|
|
8
|
+
subject { described_class.new(primary, secondary, options) }
|
|
9
|
+
|
|
10
|
+
let(:primary) { Flipper::Adapters::Memory.new }
|
|
11
|
+
let(:secondary) { Flipper::Adapters::Memory.new }
|
|
12
|
+
let(:options) { {} }
|
|
13
|
+
let(:flipper) { Flipper.new(subject) }
|
|
14
|
+
|
|
15
|
+
context 'when the primary is a functioning adapter' do
|
|
16
|
+
it_should_behave_like 'a flipper adapter'
|
|
17
|
+
|
|
18
|
+
it 'should not call the secondary' do
|
|
19
|
+
expect(secondary).not_to receive(:features)
|
|
20
|
+
subject.features
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'should not write to secondary' do
|
|
24
|
+
expect(secondary).not_to receive(:add)
|
|
25
|
+
expect(secondary).not_to receive(:enable)
|
|
26
|
+
|
|
27
|
+
flipper[:flag].enable
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'when dual_write is enabled' do
|
|
31
|
+
let(:options) { { dual_write: true } }
|
|
32
|
+
|
|
33
|
+
it_should_behave_like 'a flipper adapter'
|
|
34
|
+
|
|
35
|
+
it 'writes to both primary and secondary' do
|
|
36
|
+
expect(primary).to receive(:add).and_call_original
|
|
37
|
+
expect(primary).to receive(:enable).and_call_original
|
|
38
|
+
|
|
39
|
+
expect(secondary).to receive(:add)
|
|
40
|
+
expect(secondary).to receive(:enable)
|
|
41
|
+
|
|
42
|
+
flipper[:flag].enable
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'when primary fails during read operations' do
|
|
48
|
+
before do
|
|
49
|
+
allow(primary).to receive(:features).and_raise(Redis::ConnectionError)
|
|
50
|
+
allow(primary).to receive(:get).and_raise(Dalli::NetworkError)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'fails over to the secondary adapter for reads' do
|
|
54
|
+
expect(secondary).to receive(:features)
|
|
55
|
+
subject.features
|
|
56
|
+
|
|
57
|
+
flipper[:flag].enable
|
|
58
|
+
expect(secondary).to receive(:get).and_call_original
|
|
59
|
+
flipper[:flag].enabled?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context 'when dual_write is enabled' do
|
|
63
|
+
let(:options) { { dual_write: true } }
|
|
64
|
+
|
|
65
|
+
it_should_behave_like 'a flipper adapter'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'when primary fails during write operations' do
|
|
70
|
+
before do
|
|
71
|
+
allow(primary).to receive(:add).and_raise(PStore::Error)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
let(:options) { { dual_write: true } }
|
|
75
|
+
|
|
76
|
+
it 'fails and does not write to secondary adapter' do
|
|
77
|
+
expect(secondary).not_to receive(:add)
|
|
78
|
+
expect(secondary).not_to receive(:enable)
|
|
79
|
+
|
|
80
|
+
expect { flipper[:flag].enable }.to raise_error(PStore::Error)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'when primary is instrumented and fails' do
|
|
85
|
+
before do
|
|
86
|
+
allow(memory_adapter).to receive(:get).and_raise(Net::ReadTimeout)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
let(:memory_adapter) { Flipper::Adapters::Memory.new }
|
|
90
|
+
let(:primary) do
|
|
91
|
+
Flipper::Adapters::Instrumented.new(
|
|
92
|
+
memory_adapter,
|
|
93
|
+
instrumenter: instrumenter,
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
let(:instrumenter) { Flipper::Instrumenters::Memory.new }
|
|
97
|
+
|
|
98
|
+
it 'logs the raised exception' do
|
|
99
|
+
flipper[:flag].enabled?
|
|
100
|
+
|
|
101
|
+
expect(instrumenter.events.count).to be 1
|
|
102
|
+
|
|
103
|
+
payload = instrumenter.events[0].payload
|
|
104
|
+
expect(payload.keys).to include(:exception, :exception_object)
|
|
105
|
+
expect(payload[:exception_object]).to be_a Net::ReadTimeout
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context 'when adapter raises a SyntaxError' do
|
|
110
|
+
before do
|
|
111
|
+
allow(primary).to receive(:features).and_raise(SyntaxError)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'does not rescue this type by default' do
|
|
115
|
+
expect {
|
|
116
|
+
subject.features
|
|
117
|
+
}.to raise_error(SyntaxError)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
context 'when Failover adapter is configured to catch SyntaxError' do
|
|
121
|
+
let(:options) { { errors: [ SyntaxError ] } }
|
|
122
|
+
|
|
123
|
+
it 'fails over to secondary adapter' do
|
|
124
|
+
expect(secondary).to receive(:features)
|
|
125
|
+
subject.features
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'flipper/adapters/failsafe'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Flipper::Adapters::Failsafe do
|
|
4
|
+
subject { described_class.new(memory_adapter, options) }
|
|
5
|
+
|
|
6
|
+
let(:memory_adapter) { Flipper::Adapters::Memory.new }
|
|
7
|
+
let(:options) { {} }
|
|
8
|
+
let(:flipper) { Flipper.new(subject) }
|
|
9
|
+
|
|
10
|
+
it_should_behave_like 'a flipper adapter'
|
|
11
|
+
|
|
12
|
+
context 'when disaster strikes' do
|
|
13
|
+
before do
|
|
14
|
+
expect(flipper[feature.name].enable).to be(true)
|
|
15
|
+
|
|
16
|
+
(subject.methods - Object.methods).each do |method_name|
|
|
17
|
+
allow(memory_adapter).to receive(method_name).and_raise(IOError)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
let(:feature) { Flipper::Feature.new(:my_feature, subject) }
|
|
22
|
+
|
|
23
|
+
it { expect(subject.features).to eq(Set.new) }
|
|
24
|
+
it { expect(feature.add).to eq(false) }
|
|
25
|
+
it { expect(feature.remove).to eq(false) }
|
|
26
|
+
it { expect(feature.clear).to eq(false) }
|
|
27
|
+
it { expect(subject.get(feature)).to eq({}) }
|
|
28
|
+
it { expect(subject.get_multi([feature])).to eq({}) }
|
|
29
|
+
it { expect(subject.get_all).to eq({}) }
|
|
30
|
+
it { expect(feature.enable).to eq(false) }
|
|
31
|
+
it { expect(feature.disable).to eq(false) }
|
|
32
|
+
|
|
33
|
+
context 'when used via Flipper' do
|
|
34
|
+
it { expect(flipper.features).to eq(Set.new) }
|
|
35
|
+
it { expect(flipper[feature.name].enabled?).to eq(false) }
|
|
36
|
+
it { expect(flipper[feature.name].enable).to eq(false) }
|
|
37
|
+
it { expect(flipper[feature.name].disable).to eq(false) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'when there is a syntax error' do
|
|
41
|
+
let(:test) { flipper[feature.name].enabled? }
|
|
42
|
+
|
|
43
|
+
before do
|
|
44
|
+
expect(memory_adapter).to receive(:get).and_raise(SyntaxError)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'does not catch this type of error' do
|
|
48
|
+
expect { test }.to raise_error(SyntaxError)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when configured to catch SyntaxError' do
|
|
52
|
+
let(:options) { { errors: [SyntaxError] } }
|
|
53
|
+
|
|
54
|
+
it { expect(test).to eq(false) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
require 'helper'
|
|
2
1
|
require 'flipper/adapters/http'
|
|
3
2
|
require 'flipper/adapters/pstore'
|
|
4
|
-
require 'flipper/spec/shared_adapter_specs'
|
|
5
3
|
require 'rack/handler/webrick'
|
|
6
4
|
|
|
7
5
|
FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
|
|
@@ -54,6 +52,28 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
54
52
|
expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
|
|
55
53
|
expect(flipper[:search].groups_value).to eq(Set.new)
|
|
56
54
|
end
|
|
55
|
+
|
|
56
|
+
it "can import" do
|
|
57
|
+
adapter = Flipper::Adapters::Memory.new
|
|
58
|
+
source_flipper = Flipper.new(adapter)
|
|
59
|
+
source_flipper.enable_percentage_of_actors :search, 10
|
|
60
|
+
source_flipper.enable_percentage_of_time :search, 15
|
|
61
|
+
source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
|
|
62
|
+
source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
|
|
63
|
+
source_flipper.enable_group :search, :admins
|
|
64
|
+
source_flipper.enable_group :search, :employees
|
|
65
|
+
source_flipper.enable :plausible
|
|
66
|
+
source_flipper.disable :google_analytics
|
|
67
|
+
|
|
68
|
+
flipper = Flipper.new(subject)
|
|
69
|
+
flipper.import(source_flipper)
|
|
70
|
+
expect(flipper[:search].percentage_of_actors_value).to be(10)
|
|
71
|
+
expect(flipper[:search].percentage_of_time_value).to be(15)
|
|
72
|
+
expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
|
|
73
|
+
expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
|
|
74
|
+
expect(flipper[:plausible].boolean_value).to be(true)
|
|
75
|
+
expect(flipper[:google_analytics].boolean_value).to be(false)
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
it "sends default headers" do
|
|
@@ -84,7 +104,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
84
104
|
|
|
85
105
|
describe "#get_multi" do
|
|
86
106
|
it "raises error when not successful response" do
|
|
87
|
-
stub_request(:get, "http://app.com/flipper/features?keys=feature_panel")
|
|
107
|
+
stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
|
|
88
108
|
.to_return(status: 503, body: "", headers: {})
|
|
89
109
|
|
|
90
110
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
@@ -96,7 +116,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
96
116
|
|
|
97
117
|
describe "#get_all" do
|
|
98
118
|
it "raises error when not successful response" do
|
|
99
|
-
stub_request(:get, "http://app.com/flipper/features")
|
|
119
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
|
100
120
|
.to_return(status: 503, body: "", headers: {})
|
|
101
121
|
|
|
102
122
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
@@ -108,7 +128,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
108
128
|
|
|
109
129
|
describe "#features" do
|
|
110
130
|
it "raises error when not successful response" do
|
|
111
|
-
stub_request(:get, "http://app.com/flipper/features")
|
|
131
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
|
112
132
|
.to_return(status: 503, body: "", headers: {})
|
|
113
133
|
|
|
114
134
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
@@ -159,6 +179,19 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
159
179
|
stub_request(:post, /app.com/)
|
|
160
180
|
.to_return(status: 503, body: "{}", headers: {})
|
|
161
181
|
|
|
182
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
183
|
+
feature = Flipper::Feature.new(:search, adapter)
|
|
184
|
+
gate = feature.gate(:boolean)
|
|
185
|
+
thing = gate.wrap(true)
|
|
186
|
+
expect {
|
|
187
|
+
adapter.enable(feature, gate, thing)
|
|
188
|
+
}.to raise_error(Flipper::Adapters::Http::Error, "Failed with status: 503")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "doesn't raise json error if body cannot be parsed" do
|
|
192
|
+
stub_request(:post, /app.com/)
|
|
193
|
+
.to_return(status: 503, body: "barf", headers: {})
|
|
194
|
+
|
|
162
195
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
163
196
|
feature = Flipper::Feature.new(:search, adapter)
|
|
164
197
|
gate = feature.gate(:boolean)
|
|
@@ -167,6 +200,30 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
167
200
|
adapter.enable(feature, gate, thing)
|
|
168
201
|
}.to raise_error(Flipper::Adapters::Http::Error)
|
|
169
202
|
end
|
|
203
|
+
|
|
204
|
+
it "includes response information if available when raising error" do
|
|
205
|
+
api_response = {
|
|
206
|
+
"code" => "error",
|
|
207
|
+
"message" => "This feature has reached the limit to the number of " +
|
|
208
|
+
"actors per feature. Check out groups as a more flexible " +
|
|
209
|
+
"way to enable many actors.",
|
|
210
|
+
"more_info" => "https://www.flippercloud.io/docs",
|
|
211
|
+
}
|
|
212
|
+
stub_request(:post, /app.com/)
|
|
213
|
+
.to_return(status: 503, body: JSON.dump(api_response), headers: {})
|
|
214
|
+
|
|
215
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
|
216
|
+
feature = Flipper::Feature.new(:search, adapter)
|
|
217
|
+
gate = feature.gate(:boolean)
|
|
218
|
+
thing = gate.wrap(true)
|
|
219
|
+
error_message = "Failed with status: 503\n\nThis feature has reached the " +
|
|
220
|
+
"limit to the number of actors per feature. Check out " +
|
|
221
|
+
"groups as a more flexible way to enable many actors.\n" +
|
|
222
|
+
"https://www.flippercloud.io/docs"
|
|
223
|
+
expect {
|
|
224
|
+
adapter.enable(feature, gate, thing)
|
|
225
|
+
}.to raise_error(Flipper::Adapters::Http::Error, error_message)
|
|
226
|
+
end
|
|
170
227
|
end
|
|
171
228
|
|
|
172
229
|
describe "#disable" do
|
|
@@ -202,7 +259,8 @@ RSpec.describe Flipper::Adapters::Http do
|
|
|
202
259
|
let(:feature) { flipper[:feature_panel] }
|
|
203
260
|
|
|
204
261
|
before do
|
|
205
|
-
stub_request(:get, %r{\Ahttp://app.com*}).
|
|
262
|
+
stub_request(:get, %r{\Ahttp://app.com*}).
|
|
263
|
+
to_return(body: fixture_file('feature.json'))
|
|
206
264
|
end
|
|
207
265
|
|
|
208
266
|
it 'allows client to set request headers' do
|