flipper 0.26.0 → 1.3.6

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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
@@ -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: "{}")
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: "{}").
72
+ to_return(status: 200, body: "{}")
73
+ instance = described_class.new(cloud_configuration)
74
+ expect(instance.backoff_policy.min_timeout_ms).to eq(30_000)
75
+ expect(instance.backoff_policy.max_timeout_ms).to eq(120_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: "{}")
81
+ subject.call(enabled_metrics)
82
+ expect(subject.backoff_policy.retries).to eq(4) # 4 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(4)
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: "{}").
115
+ to_return(status: 429, body: "{}").
116
+ to_return(status: 200, body: "{}")
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: "{}").
124
+ to_return(status: 503, body: "{}").
125
+ to_return(status: 502, body: "{}").
126
+ to_return(status: 200, body: "{}")
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,208 @@
1
+ require 'flipper/cloud/telemetry'
2
+ require 'flipper/cloud/configuration'
3
+
4
+ RSpec.describe Flipper::Cloud::Telemetry do
5
+ before do
6
+ # Stub polling for features.
7
+ stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
8
+ to_return(status: 200, body: "{}")
9
+ end
10
+
11
+ it "phones home and does not update telemetry interval if missing" do
12
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
13
+ to_return(status: 200, body: "{}")
14
+
15
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
16
+
17
+ # Record some telemetry and stop the threads so we submit a response.
18
+ telemetry = described_class.new(cloud_configuration)
19
+ telemetry.record(Flipper::Feature::InstrumentationName, {
20
+ operation: :enabled?,
21
+ feature_name: :foo,
22
+ result: true,
23
+ })
24
+ telemetry.stop
25
+
26
+ expect(telemetry.interval).to eq(60)
27
+ expect(telemetry.timer.execution_interval).to eq(60)
28
+ expect(stub).to have_been_requested.at_least_once
29
+ end
30
+
31
+ it "phones home and updates telemetry interval if present" do
32
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
33
+ to_return(status: 200, body: "{}", headers: {"telemetry-interval" => "120"})
34
+
35
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
36
+
37
+ # Record some telemetry and stop the threads so we submit a response.
38
+ telemetry = described_class.new(cloud_configuration)
39
+ telemetry.record(Flipper::Feature::InstrumentationName, {
40
+ operation: :enabled?,
41
+ feature_name: :foo,
42
+ result: true,
43
+ })
44
+ telemetry.stop
45
+
46
+ expect(telemetry.interval).to eq(120)
47
+ expect(telemetry.timer.execution_interval).to eq(120)
48
+ expect(stub).to have_been_requested.at_least_once
49
+ end
50
+
51
+ it "phones home and requests shutdown if telemetry-shutdown header is true" do
52
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
53
+ to_return(status: 404, body: "{}", headers: {"telemetry-shutdown" => "true"})
54
+
55
+ output = StringIO.new
56
+ cloud_configuration = Flipper::Cloud::Configuration.new(
57
+ token: "test",
58
+ logger: Logger.new(output),
59
+ logging_enabled: true,
60
+ )
61
+
62
+ # Record some telemetry and stop the threads so we submit a response.
63
+ telemetry = described_class.new(cloud_configuration)
64
+ telemetry.record(Flipper::Feature::InstrumentationName, {
65
+ operation: :enabled?,
66
+ feature_name: :foo,
67
+ result: true,
68
+ })
69
+ telemetry.stop
70
+ expect(stub).to have_been_requested.at_least_once
71
+ expect(output.string).to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
72
+ end
73
+
74
+ it "phones home and does not shutdown if telemetry shutdown header is missing" do
75
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
76
+ to_return(status: 404, body: "{}", headers: {})
77
+
78
+ output = StringIO.new
79
+ cloud_configuration = Flipper::Cloud::Configuration.new(
80
+ token: "test",
81
+ logger: Logger.new(output),
82
+ logging_enabled: true,
83
+ )
84
+
85
+ # Record some telemetry and stop the threads so we submit a response.
86
+ telemetry = described_class.new(cloud_configuration)
87
+ telemetry.record(Flipper::Feature::InstrumentationName, {
88
+ operation: :enabled?,
89
+ feature_name: :foo,
90
+ result: true,
91
+ })
92
+ telemetry.stop
93
+ expect(stub).to have_been_requested.at_least_once
94
+ expect(output.string).not_to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
95
+ end
96
+
97
+ it "can update telemetry interval from error" do
98
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
99
+ to_return(status: 500, body: "{}", headers: {"telemetry-interval" => "120"})
100
+
101
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
102
+ telemetry = described_class.new(cloud_configuration)
103
+
104
+ # Override the submitter to use back off policy that doesn't actually
105
+ # sleep. If we don't then the stop below kills the working thread and the
106
+ # interval is never updated.
107
+ telemetry.submitter = ->(drained) {
108
+ Flipper::Cloud::Telemetry::Submitter.new(
109
+ cloud_configuration,
110
+ backoff_policy: FakeBackoffPolicy.new
111
+ ).call(drained)
112
+ }
113
+
114
+ # Record some telemetry and stop the threads so we submit a response.
115
+ telemetry.record(Flipper::Feature::InstrumentationName, {
116
+ operation: :enabled?,
117
+ feature_name: :foo,
118
+ result: true,
119
+ })
120
+ telemetry.stop
121
+
122
+ # Check the conig interval and the timer interval.
123
+ expect(telemetry.interval).to eq(120)
124
+ expect(telemetry.timer.execution_interval).to eq(120)
125
+ expect(stub).to have_been_requested.times(5)
126
+ end
127
+
128
+ it "doesn't try to update telemetry interval from error if not response error" do
129
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
130
+ to_raise(Net::OpenTimeout)
131
+
132
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
133
+ telemetry = described_class.new(cloud_configuration)
134
+
135
+ # Override the submitter to use back off policy that doesn't actually
136
+ # sleep. If we don't then the stop below kills the working thread and the
137
+ # interval is never updated.
138
+ telemetry.submitter = ->(drained) {
139
+ Flipper::Cloud::Telemetry::Submitter.new(
140
+ cloud_configuration,
141
+ backoff_policy: FakeBackoffPolicy.new
142
+ ).call(drained)
143
+ }
144
+
145
+ # Record some telemetry and stop the threads so we submit a response.
146
+ telemetry.record(Flipper::Feature::InstrumentationName, {
147
+ operation: :enabled?,
148
+ feature_name: :foo,
149
+ result: true,
150
+ })
151
+ telemetry.stop
152
+
153
+ expect(telemetry.interval).to eq(60)
154
+ expect(telemetry.timer.execution_interval).to eq(60)
155
+ expect(stub).to have_been_requested.times(5)
156
+ end
157
+
158
+ describe '#record' do
159
+ it "increments in metric storage" do
160
+ begin
161
+ config = Flipper::Cloud::Configuration.new(token: "test")
162
+ telemetry = described_class.new(config)
163
+ telemetry.record(Flipper::Feature::InstrumentationName, {
164
+ operation: :enabled?,
165
+ feature_name: :foo,
166
+ result: true,
167
+ })
168
+ telemetry.record(Flipper::Feature::InstrumentationName, {
169
+ operation: :enabled?,
170
+ feature_name: :foo,
171
+ result: true,
172
+ })
173
+ telemetry.record(Flipper::Feature::InstrumentationName, {
174
+ operation: :enabled?,
175
+ feature_name: :bar,
176
+ result: true,
177
+ })
178
+ telemetry.record(Flipper::Feature::InstrumentationName, {
179
+ operation: :enabled?,
180
+ feature_name: :baz,
181
+ result: true,
182
+ })
183
+ telemetry.record(Flipper::Feature::InstrumentationName, {
184
+ operation: :enabled?,
185
+ feature_name: :foo,
186
+ result: false,
187
+ })
188
+
189
+ drained = telemetry.metric_storage.drain
190
+ metrics_by_key = drained.keys.group_by(&:key)
191
+
192
+ foo_true, foo_false = metrics_by_key["foo"].partition { |metric| metric.result }
193
+ foo_true_sum = foo_true.map { |metric| drained[metric] }.sum
194
+ expect(foo_true_sum).to be(2)
195
+ foo_false_sum = foo_false.map { |metric| drained[metric] }.sum
196
+ expect(foo_false_sum).to be(1)
197
+
198
+ bar_true_sum = metrics_by_key["bar"].map { |metric| drained[metric] }.sum
199
+ expect(bar_true_sum).to be(1)
200
+
201
+ baz_true_sum = metrics_by_key["baz"].map { |metric| drained[metric] }.sum
202
+ expect(baz_true_sum).to be(1)
203
+ ensure
204
+ telemetry.stop
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,186 @@
1
+ require 'flipper/cloud'
2
+ require 'flipper/adapters/instrumented'
3
+ require 'flipper/instrumenters/memory'
4
+
5
+ RSpec.describe Flipper::Cloud do
6
+ before do
7
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
8
+ end
9
+
10
+ context "initialize with token" do
11
+ let(:token) { 'asdf' }
12
+
13
+ before do
14
+ @instance = described_class.new(token: token)
15
+ end
16
+
17
+ it 'returns Flipper::DSL instance' do
18
+ expect(@instance).to be_instance_of(Flipper::Cloud::DSL)
19
+ end
20
+
21
+ it 'can read the cloud configuration' do
22
+ expect(@instance.cloud_configuration).to be_instance_of(Flipper::Cloud::Configuration)
23
+ end
24
+
25
+ it 'configures the correct adapter' do
26
+ # pardon the nesting...
27
+ memoized_adapter = @instance.adapter
28
+ dual_write_adapter = memoized_adapter.adapter
29
+ expect(dual_write_adapter).to be_instance_of(Flipper::Adapters::DualWrite)
30
+ poll_adapter = dual_write_adapter.local
31
+ expect(poll_adapter).to be_instance_of(Flipper::Adapters::Poll)
32
+
33
+ http_adapter = dual_write_adapter.remote
34
+ client = http_adapter.client
35
+ expect(client.uri.scheme).to eq('https')
36
+ expect(client.uri.host).to eq('www.flippercloud.io')
37
+ expect(client.uri.path).to eq('/adapter')
38
+ expect(client.headers["flipper-cloud-token"]).to eq(token)
39
+ expect(@instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
40
+ expect(@instance.instrumenter.instrumenter).to be(Flipper::Instrumenters::Noop)
41
+ end
42
+ end
43
+
44
+ context 'initialize with token and options' do
45
+ it 'sets correct url' do
46
+ stub_request(:any, %r{fakeflipper.com}).to_return(status: 200)
47
+ instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
48
+ # pardon the nesting...
49
+ memoized = instance.adapter
50
+ dual_write = memoized.adapter
51
+ remote = dual_write.remote
52
+ uri = remote.client.uri
53
+ expect(uri.scheme).to eq('https')
54
+ expect(uri.host).to eq('www.fakeflipper.com')
55
+ expect(uri.path).to eq('/sadpanda')
56
+ end
57
+ end
58
+
59
+ it 'can initialize with no token explicitly provided' do
60
+ ENV['FLIPPER_CLOUD_TOKEN'] = 'asdf'
61
+ expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
62
+ end
63
+
64
+ it 'can set instrumenter' do
65
+ instrumenter = Flipper::Instrumenters::Memory.new
66
+ instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
67
+ expect(instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
68
+ expect(instance.instrumenter.instrumenter).to be(instrumenter)
69
+ end
70
+
71
+ it 'allows wrapping adapter with another adapter like the instrumenter' do
72
+ instance = described_class.new(token: 'asdf') do |config|
73
+ config.adapter do |adapter|
74
+ Flipper::Adapters::Instrumented.new(adapter)
75
+ end
76
+ end
77
+ # instance.adapter is memoizable adapter instance
78
+ expect(instance.adapter.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
79
+ end
80
+
81
+ it 'can set debug_output' do
82
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
83
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
84
+ .with(hash_including(debug_output: STDOUT)).at_least(:once).and_return(instance)
85
+ described_class.new(token: 'asdf', debug_output: STDOUT)
86
+ end
87
+
88
+ it 'can set read_timeout' do
89
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
90
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
91
+ .with(hash_including(read_timeout: 1)).at_least(:once).and_return(instance)
92
+ described_class.new(token: 'asdf', read_timeout: 1)
93
+ end
94
+
95
+ it 'can set open_timeout' do
96
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
97
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
98
+ .with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
99
+ described_class.new(token: 'asdf', open_timeout: 1)
100
+ end
101
+
102
+ if RUBY_VERSION >= '2.6.0'
103
+ it 'can set write_timeout' do
104
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
105
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
106
+ .with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
107
+ described_class.new(token: 'asdf', open_timeout: 1)
108
+ end
109
+ end
110
+
111
+ it 'can import' do
112
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
113
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
114
+
115
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
116
+
117
+ flipper.enable(:test)
118
+ flipper.enable(:search)
119
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
120
+ flipper.enable_percentage_of_time(:logging, 5)
121
+
122
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
123
+
124
+ get_all = {
125
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
126
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
127
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
128
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
129
+ }
130
+
131
+ expect(flipper.adapter.get_all).to eq(get_all)
132
+ cloud_flipper.import(flipper)
133
+ expect(flipper.adapter.get_all).to eq(get_all)
134
+ expect(cloud_flipper.adapter.get_all).to eq(get_all)
135
+ end
136
+
137
+ it 'raises error for failure while importing' do
138
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
139
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 500, body: "{}")
140
+
141
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
142
+
143
+ flipper.enable(:test)
144
+ flipper.enable(:search)
145
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
146
+ flipper.enable_percentage_of_time(:logging, 5)
147
+
148
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
149
+
150
+ get_all = {
151
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
152
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
153
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
154
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
155
+ }
156
+
157
+ expect(flipper.adapter.get_all).to eq(get_all)
158
+ expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
159
+ expect(flipper.adapter.get_all).to eq(get_all)
160
+ end
161
+
162
+ it 'raises error for timeout while importing' do
163
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
164
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_timeout
165
+
166
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
167
+
168
+ flipper.enable(:test)
169
+ flipper.enable(:search)
170
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
171
+ flipper.enable_percentage_of_time(:logging, 5)
172
+
173
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
174
+
175
+ get_all = {
176
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
177
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
178
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
179
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
180
+ }
181
+
182
+ expect(flipper.adapter.get_all).to eq(get_all)
183
+ expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
184
+ expect(flipper.adapter.get_all).to eq(get_all)
185
+ end
186
+ end
@@ -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