flipper 1.0.0 → 1.1.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.
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)