flipper 0.16.0 → 1.4.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 (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -0,0 +1,107 @@
1
+ require 'flipper/cloud/telemetry/backoff_policy'
2
+
3
+ RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
4
+ context "#initialize" do
5
+ it "with no options" do
6
+ policy = described_class.new
7
+ expect(policy.min_timeout_ms).to eq(30_000)
8
+ expect(policy.max_timeout_ms).to eq(120_000)
9
+ expect(policy.multiplier).to eq(1.5)
10
+ expect(policy.randomization_factor).to eq(0.5)
11
+ end
12
+
13
+ it "with options" do
14
+ policy = described_class.new({
15
+ min_timeout_ms: 1234,
16
+ max_timeout_ms: 5678,
17
+ multiplier: 24,
18
+ randomization_factor: 0.4,
19
+ })
20
+ expect(policy.min_timeout_ms).to eq(1234)
21
+ expect(policy.max_timeout_ms).to eq(5678)
22
+ expect(policy.multiplier).to eq(24)
23
+ expect(policy.randomization_factor).to eq(0.4)
24
+ end
25
+
26
+ it "with min higher than max" do
27
+ expect {
28
+ described_class.new({
29
+ min_timeout_ms: 2,
30
+ max_timeout_ms: 1,
31
+ })
32
+ }.to raise_error(ArgumentError, ":min_timeout_ms (2) must be <= :max_timeout_ms (1)")
33
+ end
34
+
35
+ it "with invalid min_timeout_ms" do
36
+ expect {
37
+ described_class.new({
38
+ min_timeout_ms: -1,
39
+ })
40
+ }.to raise_error(ArgumentError, ":min_timeout_ms must be >= 0 but was -1")
41
+ end
42
+
43
+ it "with invalid max_timeout_ms" do
44
+ expect {
45
+ described_class.new({
46
+ max_timeout_ms: -1,
47
+ })
48
+ }.to raise_error(ArgumentError, ":max_timeout_ms must be >= 0 but was -1")
49
+ end
50
+
51
+ it "from env" do
52
+ ENV.update(
53
+ "FLIPPER_BACKOFF_MIN_TIMEOUT_MS" => "1000",
54
+ "FLIPPER_BACKOFF_MAX_TIMEOUT_MS" => "2000",
55
+ "FLIPPER_BACKOFF_MULTIPLIER" => "1.9",
56
+ "FLIPPER_BACKOFF_RANDOMIZATION_FACTOR" => "0.1",
57
+ )
58
+
59
+ policy = described_class.new
60
+ expect(policy.min_timeout_ms).to eq(1000)
61
+ expect(policy.max_timeout_ms).to eq(2000)
62
+ expect(policy.multiplier).to eq(1.9)
63
+ expect(policy.randomization_factor).to eq(0.1)
64
+ end
65
+ end
66
+
67
+ context "#next_interval" do
68
+ it "works" do
69
+ policy = described_class.new({
70
+ min_timeout_ms: 1_000,
71
+ max_timeout_ms: 10_000,
72
+ multiplier: 2,
73
+ randomization_factor: 0.5,
74
+ })
75
+
76
+ expect(policy.next_interval).to be_within(500).of(1000)
77
+ expect(policy.next_interval).to be_within(1000).of(2000)
78
+ expect(policy.next_interval).to be_within(2000).of(4000)
79
+ expect(policy.next_interval).to be_within(4000).of(8000)
80
+ end
81
+
82
+ it "caps maximum duration at max_timeout_secs" do
83
+ policy = described_class.new({
84
+ min_timeout_ms: 1_000,
85
+ max_timeout_ms: 10_000,
86
+ multiplier: 2,
87
+ randomization_factor: 0.5,
88
+ })
89
+ 10.times { policy.next_interval }
90
+ expect(policy.next_interval).to be_within(10_000*0.1).of(10_000)
91
+ end
92
+ end
93
+
94
+ it "can reset" do
95
+ policy = described_class.new({
96
+ min_timeout_ms: 1_000,
97
+ max_timeout_ms: 10_000,
98
+ multiplier: 2,
99
+ randomization_factor: 0.5,
100
+ })
101
+ 10.times { policy.next_interval }
102
+
103
+ expect(policy.attempts).to eq(10)
104
+ policy.reset
105
+ expect(policy.attempts).to eq(0)
106
+ end
107
+ end
@@ -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: "{}")
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.at_least_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