flipper 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +7 -3
  4. data/.github/workflows/examples.yml +27 -5
  5. data/Changelog.md +42 -0
  6. data/Gemfile +4 -4
  7. data/README.md +13 -11
  8. data/benchmark/typecast_ips.rb +8 -0
  9. data/docs/images/flipper_cloud.png +0 -0
  10. data/examples/cloud/backoff_policy.rb +13 -0
  11. data/examples/cloud/cloud_setup.rb +16 -0
  12. data/examples/cloud/forked.rb +7 -2
  13. data/examples/cloud/threaded.rb +15 -18
  14. data/examples/expressions.rb +213 -0
  15. data/examples/strict.rb +18 -0
  16. data/flipper.gemspec +1 -2
  17. data/lib/flipper/actor.rb +6 -3
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapter_builder.rb +44 -0
  20. data/lib/flipper/adapters/dual_write.rb +1 -3
  21. data/lib/flipper/adapters/failover.rb +0 -4
  22. data/lib/flipper/adapters/failsafe.rb +0 -4
  23. data/lib/flipper/adapters/http/client.rb +26 -7
  24. data/lib/flipper/adapters/http/error.rb +1 -1
  25. data/lib/flipper/adapters/http.rb +18 -13
  26. data/lib/flipper/adapters/instrumented.rb +0 -4
  27. data/lib/flipper/adapters/memoizable.rb +14 -19
  28. data/lib/flipper/adapters/memory.rb +4 -6
  29. data/lib/flipper/adapters/operation_logger.rb +0 -4
  30. data/lib/flipper/adapters/poll.rb +1 -3
  31. data/lib/flipper/adapters/pstore.rb +17 -11
  32. data/lib/flipper/adapters/read_only.rb +4 -4
  33. data/lib/flipper/adapters/strict.rb +47 -0
  34. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  35. data/lib/flipper/adapters/sync.rb +0 -4
  36. data/lib/flipper/cloud/configuration.rb +121 -52
  37. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  38. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  39. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  40. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  41. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  42. data/lib/flipper/cloud/telemetry.rb +183 -0
  43. data/lib/flipper/configuration.rb +25 -4
  44. data/lib/flipper/dsl.rb +51 -0
  45. data/lib/flipper/engine.rb +28 -3
  46. data/lib/flipper/exporters/json/export.rb +1 -1
  47. data/lib/flipper/exporters/json/v1.rb +1 -1
  48. data/lib/flipper/expression/builder.rb +73 -0
  49. data/lib/flipper/expression/constant.rb +25 -0
  50. data/lib/flipper/expression.rb +71 -0
  51. data/lib/flipper/expressions/all.rb +11 -0
  52. data/lib/flipper/expressions/any.rb +9 -0
  53. data/lib/flipper/expressions/boolean.rb +9 -0
  54. data/lib/flipper/expressions/comparable.rb +13 -0
  55. data/lib/flipper/expressions/duration.rb +28 -0
  56. data/lib/flipper/expressions/equal.rb +9 -0
  57. data/lib/flipper/expressions/greater_than.rb +9 -0
  58. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  59. data/lib/flipper/expressions/less_than.rb +9 -0
  60. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  61. data/lib/flipper/expressions/not_equal.rb +9 -0
  62. data/lib/flipper/expressions/now.rb +9 -0
  63. data/lib/flipper/expressions/number.rb +9 -0
  64. data/lib/flipper/expressions/percentage.rb +9 -0
  65. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  66. data/lib/flipper/expressions/property.rb +9 -0
  67. data/lib/flipper/expressions/random.rb +9 -0
  68. data/lib/flipper/expressions/string.rb +9 -0
  69. data/lib/flipper/expressions/time.rb +9 -0
  70. data/lib/flipper/feature.rb +55 -0
  71. data/lib/flipper/gate.rb +1 -0
  72. data/lib/flipper/gate_values.rb +5 -2
  73. data/lib/flipper/gates/expression.rb +75 -0
  74. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  75. data/lib/flipper/middleware/memoizer.rb +29 -13
  76. data/lib/flipper/poller.rb +1 -1
  77. data/lib/flipper/serializers/gzip.rb +24 -0
  78. data/lib/flipper/serializers/json.rb +19 -0
  79. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  80. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  81. data/lib/flipper/typecast.rb +34 -6
  82. data/lib/flipper/types/percentage.rb +1 -1
  83. data/lib/flipper/version.rb +1 -1
  84. data/lib/flipper.rb +38 -1
  85. data/spec/flipper/adapter_builder_spec.rb +73 -0
  86. data/spec/flipper/adapter_spec.rb +1 -0
  87. data/spec/flipper/adapters/http_spec.rb +39 -5
  88. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  89. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  90. data/spec/flipper/adapters/strict_spec.rb +62 -0
  91. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  92. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  93. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  94. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  95. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  96. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  97. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  98. data/spec/flipper/cloud_spec.rb +12 -12
  99. data/spec/flipper/configuration_spec.rb +17 -0
  100. data/spec/flipper/dsl_spec.rb +39 -0
  101. data/spec/flipper/engine_spec.rb +108 -7
  102. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  103. data/spec/flipper/expression/builder_spec.rb +248 -0
  104. data/spec/flipper/expression_spec.rb +188 -0
  105. data/spec/flipper/expressions/all_spec.rb +15 -0
  106. data/spec/flipper/expressions/any_spec.rb +15 -0
  107. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  108. data/spec/flipper/expressions/duration_spec.rb +43 -0
  109. data/spec/flipper/expressions/equal_spec.rb +24 -0
  110. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  111. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  112. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  114. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  115. data/spec/flipper/expressions/now_spec.rb +11 -0
  116. data/spec/flipper/expressions/number_spec.rb +21 -0
  117. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  118. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  119. data/spec/flipper/expressions/property_spec.rb +13 -0
  120. data/spec/flipper/expressions/random_spec.rb +9 -0
  121. data/spec/flipper/expressions/string_spec.rb +11 -0
  122. data/spec/flipper/expressions/time_spec.rb +13 -0
  123. data/spec/flipper/feature_spec.rb +360 -1
  124. data/spec/flipper/gate_values_spec.rb +2 -2
  125. data/spec/flipper/gates/expression_spec.rb +108 -0
  126. data/spec/flipper/identifier_spec.rb +4 -5
  127. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  128. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  129. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  130. data/spec/flipper/serializers/json_spec.rb +13 -0
  131. data/spec/flipper/typecast_spec.rb +43 -7
  132. data/spec/flipper/types/actor_spec.rb +18 -1
  133. data/spec/flipper_integration_spec.rb +102 -4
  134. data/spec/flipper_spec.rb +89 -1
  135. data/spec/spec_helper.rb +5 -0
  136. data/spec/support/actor_names.yml +1 -0
  137. data/spec/support/fake_backoff_policy.rb +15 -0
  138. data/spec/support/spec_helpers.rb +11 -3
  139. metadata +104 -18
  140. data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -0,0 +1,87 @@
1
+ require 'flipper/cloud/telemetry/metric'
2
+
3
+ RSpec.describe Flipper::Cloud::Telemetry::Metric do
4
+ it 'has key, result and time' do
5
+ metric = described_class.new(:search, true, 1696793160)
6
+ expect(metric.key).to eq(:search)
7
+ expect(metric.result).to eq(true)
8
+ expect(metric.time).to eq(1696793160)
9
+ end
10
+
11
+ it "clamps time to minute" do
12
+ metric = described_class.new(:search, true, 1696793204)
13
+ expect(metric.time).to eq(1696793160)
14
+ end
15
+
16
+ describe "#eql?" do
17
+ it "returns true when key, time and result are the same" do
18
+ metric = described_class.new(:search, true, 1696793204)
19
+ other = described_class.new(:search, true, 1696793204)
20
+ expect(metric.eql?(other)).to be(true)
21
+ end
22
+
23
+ it "returns false for other class" do
24
+ metric = described_class.new(:search, true, 1696793204)
25
+ other = Object.new
26
+ expect(metric.eql?(other)).to be(false)
27
+ end
28
+
29
+ it "returns false for sub class" do
30
+ metric = described_class.new(:search, true, 1696793204)
31
+ other = Class.new(described_class).new(:search, true, 1696793204)
32
+ expect(metric.eql?(other)).to be(false)
33
+ end
34
+
35
+ it "returns false if key is different" do
36
+ metric = described_class.new(:search, true, 1696793204)
37
+ other = described_class.new(:other, true, 1696793204)
38
+ expect(metric.eql?(other)).to be(false)
39
+ end
40
+
41
+ it "returns false if time is different" do
42
+ metric = described_class.new(:search, true, 1696793204)
43
+ other = described_class.new(:search, true, 1696793204 - 60 - 60)
44
+ expect(metric.eql?(other)).to be(false)
45
+ end
46
+
47
+ it "returns true with different times if times are in same minute" do
48
+ metric = described_class.new(:search, true, 1696793204)
49
+ other = described_class.new(:search, true, 1696793206)
50
+ expect(metric.eql?(other)).to be(true)
51
+ end
52
+
53
+ it "returns false if result is different" do
54
+ metric = described_class.new(:search, true, 1696793204)
55
+ other = described_class.new(:search, false, 1696793204)
56
+ expect(metric.eql?(other)).to be(false)
57
+ end
58
+ end
59
+
60
+ describe "#hash" do
61
+ it "returns hash based on class, key, time and result" do
62
+ metric = described_class.new(:search, true, 1696793204)
63
+ expect(metric.hash).to eq([described_class, metric.key, metric.time, metric.result].hash)
64
+ end
65
+ end
66
+
67
+ describe "#as_json" do
68
+ it "returns key time and result" do
69
+ metric = described_class.new(:search, true, 1696793160)
70
+ expect(metric.as_json).to eq({
71
+ "key" => "search",
72
+ "result" => true,
73
+ "time" => 1696793160,
74
+ })
75
+ end
76
+
77
+ it "can include other hashes" do
78
+ metric = described_class.new(:search, true, 1696793160)
79
+ expect(metric.as_json(with: {"value" => 2})).to eq({
80
+ "key" => "search",
81
+ "result" => true,
82
+ "time" => 1696793160,
83
+ "value" => 2,
84
+ })
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ require 'flipper/cloud/telemetry/metric_storage'
2
+ require 'flipper/cloud/telemetry/metric'
3
+
4
+ RSpec.describe Flipper::Cloud::Telemetry::MetricStorage do
5
+ describe "#increment" do
6
+ it "increments the counter for the metric" do
7
+ metric_storage = described_class.new
8
+ storage = metric_storage.instance_variable_get(:@storage)
9
+ metric = Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)
10
+ other = Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793160)
11
+
12
+ metric_storage.increment(metric)
13
+ expect(storage[metric].value).to be(1)
14
+
15
+ 5.times { metric_storage.increment(metric) }
16
+ expect(storage[metric].value).to be(6)
17
+
18
+ metric_storage.increment(other)
19
+ expect(storage[other].value).to be(1)
20
+ end
21
+ end
22
+
23
+ describe "#drain" do
24
+ it "returns clears metrics and return hash" do
25
+ metric_storage = described_class.new
26
+ storage = metric_storage.instance_variable_get(:@storage)
27
+ storage[Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)] = Concurrent::AtomicFixnum.new(10)
28
+ storage[Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793161)] = Concurrent::AtomicFixnum.new(15)
29
+ storage[Flipper::Cloud::Telemetry::Metric.new(:plausible, true, 1696793162)] = Concurrent::AtomicFixnum.new(25)
30
+ storage[Flipper::Cloud::Telemetry::Metric.new(:administrator, true, 1696793164)] = Concurrent::AtomicFixnum.new(1)
31
+ storage[Flipper::Cloud::Telemetry::Metric.new(:administrator, false, 1696793164)] = Concurrent::AtomicFixnum.new(24)
32
+
33
+ drained = metric_storage.drain
34
+ expect(drained).to be_frozen
35
+ expect(drained).to eq({
36
+ Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160) => 10,
37
+ Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793161) => 15,
38
+ Flipper::Cloud::Telemetry::Metric.new(:plausible, true, 1696793162) => 25,
39
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, true, 1696793164) => 1,
40
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, false, 1696793164) => 24,
41
+ })
42
+ expect(storage.keys).to eq([])
43
+ end
44
+ end
45
+
46
+ describe "#empty?" do
47
+ it "returns true if empty" do
48
+ metric_storage = described_class.new
49
+ expect(metric_storage).to be_empty
50
+ end
51
+
52
+ it "returns false if not empty" do
53
+ metric_storage = described_class.new
54
+ metric_storage.increment Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160)
55
+ expect(metric_storage).not_to be_empty
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,145 @@
1
+ require "stringio"
2
+ require 'flipper/cloud/configuration'
3
+ require 'flipper/cloud/telemetry/submitter'
4
+
5
+ RSpec.describe Flipper::Cloud::Telemetry::Submitter do
6
+ let(:cloud_configuration) {
7
+ Flipper::Cloud::Configuration.new({token: "asdf"})
8
+ }
9
+ let(:fake_backoff_policy) { FakeBackoffPolicy.new }
10
+ let(:subject) { described_class.new(cloud_configuration, backoff_policy: fake_backoff_policy) }
11
+
12
+ describe "#initialize" do
13
+ it "works with cloud_configuration" do
14
+ submitter = described_class.new(cloud_configuration)
15
+ expect(submitter.cloud_configuration).to eq(cloud_configuration)
16
+ end
17
+ end
18
+
19
+ describe "#call" do
20
+ let(:enabled_metrics) {
21
+ {
22
+ Flipper::Cloud::Telemetry::Metric.new(:search, true, 1696793160) => 10,
23
+ Flipper::Cloud::Telemetry::Metric.new(:search, false, 1696793161) => 15,
24
+ Flipper::Cloud::Telemetry::Metric.new(:plausible, true, 1696793162) => 25,
25
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, true, 1696793164) => 1,
26
+ Flipper::Cloud::Telemetry::Metric.new(:administrator, false, 1696793164) => 24,
27
+ }
28
+ }
29
+
30
+ it "does not submit blank metrics" do
31
+ expect(subject.call({})).to be(nil)
32
+ end
33
+
34
+ it "submits present metrics" do
35
+ expected_body = {
36
+ "request_id" => subject.request_id,
37
+ "enabled_metrics" =>[
38
+ {"key" => "search", "time" => 1696793160, "result" => true, "value" => 10},
39
+ {"key" => "search", "time" => 1696793160, "result" => false, "value" => 15},
40
+ {"key" => "plausible", "time" => 1696793160, "result" => true, "value" => 25},
41
+ {"key" => "administrator", "time" => 1696793160, "result" => true, "value" => 1},
42
+ {"key" => "administrator", "time" => 1696793160, "result" => false, "value" => 24},
43
+ ]
44
+ }
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}",
59
+ }
60
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
61
+ with(headers: expected_headers) { |request|
62
+ gunzipped = Flipper::Typecast.from_gzip(request.body)
63
+ body = Flipper::Typecast.from_json(gunzipped)
64
+ body == expected_body
65
+ }.to_return(status: 200, body: "{}", headers: {})
66
+ subject.call(enabled_metrics)
67
+ end
68
+
69
+ it "defaults backoff_policy" do
70
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
71
+ to_return(status: 429, body: "{}", headers: {}).
72
+ to_return(status: 200, body: "{}", headers: {})
73
+ instance = described_class.new(cloud_configuration)
74
+ expect(instance.backoff_policy.min_timeout_ms).to eq(1_000)
75
+ expect(instance.backoff_policy.max_timeout_ms).to eq(30_000)
76
+ end
77
+
78
+ it "tries 10 times by default" do
79
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
80
+ to_return(status: 500, body: "{}", headers: {})
81
+ subject.call(enabled_metrics)
82
+ expect(subject.backoff_policy.retries).to eq(9) # 9 retries + 1 initial attempt
83
+ end
84
+
85
+ [
86
+ EOFError,
87
+ Errno::ECONNABORTED,
88
+ Errno::ECONNREFUSED,
89
+ Errno::ECONNRESET,
90
+ Errno::EHOSTUNREACH,
91
+ Errno::EINVAL,
92
+ Errno::ENETUNREACH,
93
+ Errno::ENOTSOCK,
94
+ Errno::EPIPE,
95
+ Errno::ETIMEDOUT,
96
+ Net::HTTPBadResponse,
97
+ Net::HTTPHeaderSyntaxError,
98
+ Net::ProtocolError,
99
+ Net::ReadTimeout,
100
+ OpenSSL::SSL::SSLError,
101
+ SocketError,
102
+ Timeout::Error, # Also covers subclasses like Net::OpenTimeout.
103
+ ].each do |error_class|
104
+ it "retries on #{error_class}" do
105
+ stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
106
+ to_raise(error_class)
107
+ subject.call(enabled_metrics)
108
+ expect(subject.backoff_policy.retries).to eq(9)
109
+ end
110
+ end
111
+
112
+ it "retries on 429" do
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: {})
117
+ subject.call(enabled_metrics)
118
+ expect(subject.backoff_policy.retries).to eq(2)
119
+ end
120
+
121
+ it "retries on 500" do
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: {})
127
+ subject.call(enabled_metrics)
128
+ expect(subject.backoff_policy.retries).to eq(3)
129
+ end
130
+ end
131
+
132
+ def with_telemetry_debug_logging(&block)
133
+ output = StringIO.new
134
+ original_logger = cloud_configuration.logger
135
+
136
+ begin
137
+ cloud_configuration.logger = Logger.new(output)
138
+ block.call
139
+ ensure
140
+ cloud_configuration.logger = original_logger
141
+ end
142
+
143
+ output.string
144
+ end
145
+ end
@@ -0,0 +1,156 @@
1
+ require 'flipper/cloud/telemetry'
2
+ require 'flipper/cloud/configuration'
3
+
4
+ RSpec.describe Flipper::Cloud::Telemetry do
5
+ it "phones home and does not update telemetry interval if missing" do
6
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
7
+ to_return(status: 200, body: "{}", headers: {})
8
+
9
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
10
+
11
+ # Record some telemetry and stop the threads so we submit a response.
12
+ telemetry = described_class.new(cloud_configuration)
13
+ telemetry.record(Flipper::Feature::InstrumentationName, {
14
+ operation: :enabled?,
15
+ feature_name: :foo,
16
+ result: true,
17
+ })
18
+ telemetry.stop
19
+
20
+ expect(telemetry.interval).to eq(60)
21
+ expect(telemetry.timer.execution_interval).to eq(60)
22
+ expect(stub).to have_been_requested
23
+ end
24
+
25
+ it "phones home and updates telemetry interval if present" do
26
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
27
+ to_return(status: 200, body: "{}", headers: {"telemetry-interval" => "120"})
28
+
29
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
30
+
31
+ # Record some telemetry and stop the threads so we submit a response.
32
+ telemetry = described_class.new(cloud_configuration)
33
+ telemetry.record(Flipper::Feature::InstrumentationName, {
34
+ operation: :enabled?,
35
+ feature_name: :foo,
36
+ result: true,
37
+ })
38
+ telemetry.stop
39
+
40
+ expect(telemetry.interval).to eq(120)
41
+ expect(telemetry.timer.execution_interval).to eq(120)
42
+ expect(stub).to have_been_requested
43
+ end
44
+
45
+ it "can update telemetry interval from error" do
46
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
47
+ to_return(status: 500, body: "{}", headers: {"telemetry-interval" => "120"})
48
+
49
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
50
+ telemetry = described_class.new(cloud_configuration)
51
+
52
+ # Override the submitter to use back off policy that doesn't actually
53
+ # sleep. If we don't then the stop below kills the working thread and the
54
+ # interval is never updated.
55
+ telemetry.submitter = ->(drained) {
56
+ Flipper::Cloud::Telemetry::Submitter.new(
57
+ cloud_configuration,
58
+ backoff_policy: FakeBackoffPolicy.new
59
+ ).call(drained)
60
+ }
61
+
62
+ # Record some telemetry and stop the threads so we submit a response.
63
+ telemetry.record(Flipper::Feature::InstrumentationName, {
64
+ operation: :enabled?,
65
+ feature_name: :foo,
66
+ result: true,
67
+ })
68
+ telemetry.stop
69
+
70
+ # Check the conig interval and the timer interval.
71
+ expect(telemetry.interval).to eq(120)
72
+ expect(telemetry.timer.execution_interval).to eq(120)
73
+ expect(stub).to have_been_requested.times(10)
74
+ end
75
+
76
+ it "doesn't try to update telemetry interval from error if not response error" do
77
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
78
+ to_raise(Net::OpenTimeout)
79
+
80
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
81
+ telemetry = described_class.new(cloud_configuration)
82
+
83
+ # Override the submitter to use back off policy that doesn't actually
84
+ # sleep. If we don't then the stop below kills the working thread and the
85
+ # interval is never updated.
86
+ telemetry.submitter = ->(drained) {
87
+ Flipper::Cloud::Telemetry::Submitter.new(
88
+ cloud_configuration,
89
+ backoff_policy: FakeBackoffPolicy.new
90
+ ).call(drained)
91
+ }
92
+
93
+ # Record some telemetry and stop the threads so we submit a response.
94
+ telemetry.record(Flipper::Feature::InstrumentationName, {
95
+ operation: :enabled?,
96
+ feature_name: :foo,
97
+ result: true,
98
+ })
99
+ telemetry.stop
100
+
101
+ expect(telemetry.interval).to eq(60)
102
+ expect(telemetry.timer.execution_interval).to eq(60)
103
+ expect(stub).to have_been_requested.times(10)
104
+ end
105
+
106
+ describe '#record' do
107
+ it "increments in metric storage" do
108
+ begin
109
+ config = Flipper::Cloud::Configuration.new(token: "test")
110
+ telemetry = described_class.new(config)
111
+ telemetry.record(Flipper::Feature::InstrumentationName, {
112
+ operation: :enabled?,
113
+ feature_name: :foo,
114
+ result: true,
115
+ })
116
+ telemetry.record(Flipper::Feature::InstrumentationName, {
117
+ operation: :enabled?,
118
+ feature_name: :foo,
119
+ result: true,
120
+ })
121
+ telemetry.record(Flipper::Feature::InstrumentationName, {
122
+ operation: :enabled?,
123
+ feature_name: :bar,
124
+ result: true,
125
+ })
126
+ telemetry.record(Flipper::Feature::InstrumentationName, {
127
+ operation: :enabled?,
128
+ feature_name: :baz,
129
+ result: true,
130
+ })
131
+ telemetry.record(Flipper::Feature::InstrumentationName, {
132
+ operation: :enabled?,
133
+ feature_name: :foo,
134
+ result: false,
135
+ })
136
+
137
+ drained = telemetry.metric_storage.drain
138
+ metrics_by_key = drained.keys.group_by(&:key)
139
+
140
+ foo_true, foo_false = metrics_by_key["foo"].partition { |metric| metric.result }
141
+ foo_true_sum = foo_true.map { |metric| drained[metric] }.sum
142
+ expect(foo_true_sum).to be(2)
143
+ foo_false_sum = foo_false.map { |metric| drained[metric] }.sum
144
+ expect(foo_false_sum).to be(1)
145
+
146
+ bar_true_sum = metrics_by_key["bar"].map { |metric| drained[metric] }.sum
147
+ expect(bar_true_sum).to be(1)
148
+
149
+ baz_true_sum = metrics_by_key["baz"].map { |metric| drained[metric] }.sum
150
+ expect(baz_true_sum).to be(1)
151
+ ensure
152
+ telemetry.stop
153
+ end
154
+ end
155
+ end
156
+ end
@@ -116,10 +116,10 @@ RSpec.describe Flipper::Cloud do
116
116
  cloud_flipper = Flipper::Cloud.new(token: "asdf")
117
117
 
118
118
  get_all = {
119
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
120
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
121
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
122
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
119
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
120
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
121
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
122
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
123
123
  }
124
124
 
125
125
  expect(flipper.adapter.get_all).to eq(get_all)
@@ -142,10 +142,10 @@ RSpec.describe Flipper::Cloud do
142
142
  cloud_flipper = Flipper::Cloud.new(token: "asdf")
143
143
 
144
144
  get_all = {
145
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
146
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
147
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
148
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
145
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
146
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
147
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
148
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
149
149
  }
150
150
 
151
151
  expect(flipper.adapter.get_all).to eq(get_all)
@@ -167,10 +167,10 @@ RSpec.describe Flipper::Cloud do
167
167
  cloud_flipper = Flipper::Cloud.new(token: "asdf")
168
168
 
169
169
  get_all = {
170
- "logging" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: "5"},
171
- "search" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
172
- "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
173
- "test" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
170
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
171
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
172
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
173
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
174
174
  }
175
175
 
176
176
  expect(flipper.adapter.get_all).to eq(get_all)
@@ -30,4 +30,21 @@ RSpec.describe Flipper::Configuration do
30
30
  expect(subject.default).to be(instance)
31
31
  end
32
32
  end
33
+
34
+ describe '#statsd' do
35
+ let(:statsd) { double(Statsd) }
36
+
37
+ after do
38
+ Flipper::Instrumentation::StatsdSubscriber.client = nil
39
+ end
40
+
41
+ it 'returns nil by default' do
42
+ expect(subject.statsd).to be_nil
43
+ end
44
+
45
+ it 'can be set' do
46
+ subject.statsd = statsd
47
+ expect(subject.statsd).to be(statsd)
48
+ end
49
+ end
33
50
  end
@@ -136,6 +136,18 @@ RSpec.describe Flipper::DSL do
136
136
  end
137
137
  end
138
138
 
139
+ describe '#expression' do
140
+ it "returns nil if feature has no expression" do
141
+ expect(subject.expression(:stats)).to be(nil)
142
+ end
143
+
144
+ it "returns expression if feature has expression" do
145
+ expression = Flipper.property(:plan).eq("basic")
146
+ subject[:stats].enable_expression expression
147
+ expect(subject.expression(:stats)).to eq(expression)
148
+ end
149
+ end
150
+
139
151
  describe '#features' do
140
152
  context 'with no features enabled/disabled' do
141
153
  it 'defaults to empty set' do
@@ -171,6 +183,33 @@ RSpec.describe Flipper::DSL do
171
183
  end
172
184
  end
173
185
 
186
+ describe '#enable_expression/disable_expression' do
187
+ it 'enables and disables the feature for the expression' do
188
+ expression = Flipper.property(:plan).eq("basic")
189
+
190
+ expect(subject[:stats].expression).to be(nil)
191
+ subject.enable_expression(:stats, expression)
192
+ expect(subject[:stats].expression).to eq(expression)
193
+
194
+ subject.disable_expression(:stats)
195
+ expect(subject[:stats].expression).to be(nil)
196
+ end
197
+ end
198
+
199
+ describe '#add_expression/remove_expression' do
200
+ it 'enables and disables the feature for the expression' do
201
+ expression = Flipper.property(:plan).eq("basic")
202
+ any_expression = Flipper.any(expression)
203
+
204
+ expect(subject[:stats].expression).to be(nil)
205
+ subject.add_expression(:stats, any_expression)
206
+ expect(subject[:stats].expression).to eq(any_expression)
207
+
208
+ subject.remove_expression(:stats, expression)
209
+ expect(subject[:stats].expression).to eq(Flipper.any)
210
+ end
211
+ end
212
+
174
213
  describe '#enable_actor/disable_actor' do
175
214
  it 'enables and disables the feature for actor' do
176
215
  actor = Flipper::Actor.new(5)