verdict 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e99d3cb09e2dbfb3cafeb6f05171978721cd5a79fd4570f8b6bd6c6e4975e17
4
- data.tar.gz: 4a885cb9d8a991046e3a22e428a35982bf7565b03aaa49fa0da0b9963e34bc56
3
+ metadata.gz: ec6df6bdd38dc947ff1072e1dce844ab4d3ddd522701c8de578a35b4c9c51be8
4
+ data.tar.gz: e6b62b2c579a363f37526079389bea4cb16eb28ed7573dec2d3fa8f8c74ee4ed
5
5
  SHA512:
6
- metadata.gz: 89c2d20c1892c630942099ac0b0ea1d3db012a6aaa9031a7bbf8d834fad295adfaf4cc0194d09930608c4416bbeb8efba89852446e02b091c237d851d8266704
7
- data.tar.gz: cdb0271d66903d1f39316c2665c68bf8deae733c105e3aecc8bb665adaef4981129bb0f7cce6baf001eb7f246af50e40d85115bf3105525a44b2111fb2c41fbc
6
+ metadata.gz: 1399fa37c4575888ffb83aea54469784db11ceef1ebc99f5c1fa5de75b936f460b1f34dc717c28f4f19a6e969c3a85a3faad022119113cd94f754ad61b4fdb85
7
+ data.tar.gz: 7f1836b8d23ca79a4338f0a4f4ed244ebe7bdbd8b9f03529f2cb00e110c280b1033e112928da900d805112eb0036e0f74278ea021514016541accf01d8c3308c
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  dump.rdb
19
+ log
@@ -11,5 +11,6 @@ rvm:
11
11
  matrix:
12
12
  allow_failures:
13
13
  - rvm: ruby-head
14
+ - rvm: jruby
14
15
  services:
15
16
  - redis-server
@@ -1,8 +1,14 @@
1
+ ## v0.13.0
2
+
3
+ * Add optional experiment definition methods `schedule_start_timestamp` and `schedule_end_timestamp` to support limiting experiment's lifetime in a pre-determined time interval.
4
+ * Support eager loading from within a Rails app using Zeitwerk.
5
+ * Add `CookieStorage` storage backend. This backend is a distributed store for Verdict and does not support experiment timestamps. It is designed to be used with Rails applications and requires that `.cookies` be set to the `CookieJar` instance before use.
6
+
1
7
  ## v0.12.0
2
8
 
3
9
  * Allow options to be passed to `Experiment#cleanup` so they can be forwarded to storage.
4
10
 
5
- * Changed `Experiment#cleanup` to accept an argument of type `Verdict::Experiment`.
11
+ * Changed `Experiment#cleanup` to accept an argument of type `Verdict::Experiment`.
6
12
  Passing a `String`/`Symbol` argument is still supported, but will log a deprecation warning.
7
13
 
8
14
  ## v0.11.0
@@ -0,0 +1,7 @@
1
+ ## Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes, including tests (`git commit -am 'Added some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create new Pull Request
data/README.md CHANGED
@@ -19,6 +19,8 @@ Add this line to your application's Gemfile, and run `bundle install`:
19
19
 
20
20
  If you're using Rails, the Railtie will handle setting the logger to `Rails.logger` and the experiments directory to `app/experiments`. It will also load the rake tasks for you (run `bundle exec rake -T | grep experiments:` for options).
21
21
 
22
+ You may find the [Concepts](docs/concepts.md) documentation a good place to familiarise yourself with Verdict's nomenclature.
23
+
22
24
  ## Usage
23
25
 
24
26
  The `Verdict::Experiment` class is used to create an experiment, define control and experiment groups, and to qualify subjects.
@@ -42,8 +44,7 @@ Verdict::Experiment.define :my_experiment do
42
44
  end
43
45
  ```
44
46
 
45
- Usually you'll want to place this in a file called **my_experiment.rb** in the
46
- **/app/experiments** folder.
47
+ Usually you'll want to place this in a file called **my_experiment.rb** in the **/app/experiments** folder.
47
48
 
48
49
  _We recommend that you subclass `Verdict::Experiment` to set some default options for your app's environment, and call `define` on that class instead._
49
50
 
@@ -71,9 +72,9 @@ Verdict uses a very simple interface for storing experiment assignments. Out of
71
72
 
72
73
  * Memory
73
74
  * Redis
75
+ * Cookies
74
76
 
75
- You can set up storage for your experiment by calling the `storage` method with
76
- an object that responds to the following methods:
77
+ You can set up storage for your experiment by calling the `storage` method with an object that responds to the following methods:
77
78
 
78
79
  * `store_assignment(assignment)`
79
80
  * `retrieve_assignment(experiment, subject)`
@@ -83,14 +84,15 @@ an object that responds to the following methods:
83
84
 
84
85
  Regarding the method signatures above, `experiment` is the Experiment instance, `subject` is the Subject instance, and `assignment` is a `Verdict::Assignment` instance.
85
86
 
86
- The `subject` instance will be identified internally by its `subject_identifier`
87
- By default it will use `subject.id.to_s` as `subject_identifier`, but you can change that by overriding `def subject_identifier(subject)` on the experiment.
87
+ The `subject` instance will be identified internally by its `subject_identifier`. By default it will use `subject.id.to_s` as `subject_identifier`, but you can change that by overriding `def subject_identifier(subject)` on the experiment.
88
88
 
89
89
  Storage providers simply store subject assignments and require quick lookups of subject identifiers. They allow for complex (high CPU) assignments, and for assignments that might not always put the same subject in the same group by storing the assignment for later use.
90
90
 
91
- Storage providers are intended for operational use and should not be used for data analysis. For data analysis, you should use the logger.
91
+ Storage providers are intended for operational use and should not be used for data analysis. For data analysis, you should use [the logger](#logging).
92
+
93
+ When removing old experiments you might want to clean up corresponding experiment assignments, to reduce the amount of data stored and loaded. By using the logger, this data removal doesn't impact historic data or data analysis.
92
94
 
93
- For more details about these methods, check out the source code for [Verdict::Storage::MockStorage](lib/verdict/storage/mock_storage.rb)
95
+ For more details about these methods, check out the source code for [`Verdict::Storage::MockStorage`](lib/verdict/storage/mock_storage.rb)
94
96
 
95
97
  ## Logging
96
98
 
@@ -101,12 +103,3 @@ You can override the logging by overriding the `def log_assignment(assignment)`
101
103
  Logging (as opposed to storage) should be used for data analysis. The logger requires a write-only / forward-only stream to write to, e.g. a log file, Kafka, or an insert-only database table.
102
104
 
103
105
  It's possible to run an experiment without defining any storage, though this comes with several drawbacks. Logging on the other hand is required in order to analyze the results.
104
-
105
-
106
- ## Contributing
107
-
108
- 1. Fork it
109
- 2. Create your feature branch (`git checkout -b my-new-feature`)
110
- 3. Commit your changes, including tests (`git commit -am 'Added some feature'`)
111
- 4. Push to the branch (`git push origin my-new-feature`)
112
- 5. Create new Pull Request, and mention @wvanbergen.
@@ -0,0 +1,35 @@
1
+ # Verdict Concepts
2
+
3
+ Understanding the following Verdict concepts can help you implement experiments and discuss them with peers.
4
+
5
+ - [Experiment](#experiment)
6
+ - [Qualifiers](#qualifiers)
7
+ - [Subject](#subject)
8
+ - [Group](#group)
9
+ - [Assignment](#assignment)
10
+
11
+ ### Experiment
12
+
13
+ `Experiment`s are defined in `app/experiments` and encapsulate the details of the test including metadata such as the name, description or owners, as well as any [qualifiers](#qualifiers) or the [groups](#groups) available in the test.
14
+
15
+ ### Qualifiers
16
+
17
+ Qualifiers are methods that take two parameters, `subject` and `context`. They return `true` if the `subject` can take this experiment, or `false` if the `subject` is not eligible. Experiments can have multiple qualifiers which are evaluated in succession. If a qualifier returns `false`, no further qualifiers are checked.
18
+
19
+ Examples of qualifiers might be subjects that:
20
+
21
+ - Originate from certain browsers or devices
22
+ - Have a specific `Accept-Language` header
23
+ - Have added items to their cart.
24
+
25
+ ### Subject
26
+
27
+ A subject is the thing that will be [qualified](#qualifiers), and if successfull, [assigned](#assignment) to a particular [group](#group). For example, a subject might represent a user.
28
+
29
+ ### Group
30
+
31
+ Each [experiment](#experiment) will have two or more groups, usually a control group and a test group. Qualified [subjects](#subject) are assigned to one of the groups in the experiment.
32
+
33
+ ### Assignment
34
+
35
+ An assignment occurs when a qualified [subject](#subject) is placed into a [group](#group). Assignments are persisted so repeat visits by the subject skip [qualification](#qualifiers) and re-assignment and simply returns the existing assignment.
@@ -52,6 +52,17 @@ class Verdict::Experiment
52
52
  return self
53
53
  end
54
54
 
55
+ # Optional: Together with the "end timestamp", limits the experiment run timeline within
56
+ # the given time interval. When experiment is not scheduled, subject switch returns nil.
57
+ # This is useful when the experimenter requires the experiment to run in a strict timeline.
58
+ def schedule_start_timestamp(timestamp)
59
+ @schedule_start_timestamp = timestamp
60
+ end
61
+
62
+ def schedule_end_timestamp(timestamp)
63
+ @schedule_end_timestamp = timestamp
64
+ end
65
+
55
66
  def rollout_percentage(percentage, rollout_group_name = :enabled)
56
67
  groups(Verdict::Segmenters::RolloutSegmenter) do
57
68
  group rollout_group_name, percentage
@@ -172,6 +183,7 @@ class Verdict::Experiment
172
183
  end
173
184
 
174
185
  def switch(subject, context = nil)
186
+ return unless is_scheduled?
175
187
  assign(subject, context).to_sym
176
188
  end
177
189
 
@@ -239,10 +251,12 @@ class Verdict::Experiment
239
251
  def set_start_timestamp
240
252
  @storage.store_start_timestamp(self, started_now = Time.now.utc)
241
253
  started_now
254
+ rescue NotImplementedError
255
+ nil
242
256
  end
243
257
 
244
258
  def ensure_experiment_has_started
245
- @started_at ||= @storage.retrieve_start_timestamp(self) || set_start_timestamp
259
+ @started_at ||= started_at || set_start_timestamp
246
260
  rescue Verdict::StorageError
247
261
  @started_at ||= Time.now.utc
248
262
  end
@@ -250,4 +264,16 @@ class Verdict::Experiment
250
264
  def nil_assignment(subject)
251
265
  subject_assignment(subject, nil, nil)
252
266
  end
267
+
268
+ private
269
+
270
+ def is_scheduled?
271
+ if @schedule_start_timestamp and @schedule_start_timestamp > Time.now
272
+ return false
273
+ end
274
+ if @schedule_end_timestamp and @schedule_end_timestamp < Time.now
275
+ return false
276
+ end
277
+ return true
278
+ end
253
279
  end
@@ -5,10 +5,15 @@ class Verdict::Railtie < Rails::Railtie
5
5
  Verdict.default_logger = Rails.logger
6
6
 
7
7
  Verdict.directory ||= Rails.root.join('app', 'experiments')
8
- app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
9
8
 
10
- # Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
11
- app.config.eager_load_paths.freeze
9
+ if Rails.gem_version >= Gem::Version.new('6.0.0') && Rails.autoloaders.zeitwerk_enabled?
10
+ Rails.autoloaders.main.ignore(Verdict.directory)
11
+ else
12
+ app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
13
+
14
+ # Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
15
+ app.config.eager_load_paths.freeze
16
+ end
12
17
  end
13
18
 
14
19
  config.to_prepare do
@@ -1,4 +1,5 @@
1
1
  require 'verdict/storage/base_storage'
2
+ require 'verdict/storage/cookie_storage'
2
3
  require 'verdict/storage/mock_storage'
3
4
  require 'verdict/storage/memory_storage'
4
5
  require 'verdict/storage/redis_storage'
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verdict
4
+ module Storage
5
+ # CookieStorage, unlike other Verdict storage classes, is distributed and stored on the client.
6
+ # Because cookies are opaque to users, we obsfucate information about the test such as the
7
+ # human readable names for the experiment or assignment group. This means this class assumes
8
+ # that `name` will be an obsfucated value, or one that we comfortable being "public".
9
+ class CookieStorage < BaseStorage
10
+ DEFAULT_COOKIE_LIFESPAN_SECONDS = 15778476 # 6 months
11
+ KEY = :assignment
12
+
13
+ attr_accessor :cookies
14
+ attr_reader :cookie_lifespan
15
+
16
+ def initialize(cookie_lifespan: DEFAULT_COOKIE_LIFESPAN_SECONDS)
17
+ @cookies = nil
18
+ @cookie_lifespan = cookie_lifespan
19
+ end
20
+
21
+ def store_assignment(assignment)
22
+ data = {
23
+ group: digest_of(assignment.group.to_s),
24
+ created_at: assignment.created_at.strftime('%FT%TZ')
25
+ }
26
+
27
+ set(assignment.experiment.handle.to_s, KEY, JSON.dump(data))
28
+ end
29
+
30
+ def retrieve_assignment(experiment, subject)
31
+ if (value = get(experiment.handle.to_s, KEY))
32
+ data = parse_cookie_value(value)
33
+ group = find_group_by_digest(experiment, data['group'])
34
+
35
+ if group.nil?
36
+ experiment.remove_subject_assignment(subject)
37
+ return nil
38
+ end
39
+
40
+ experiment.subject_assignment(subject, group, Time.xmlschema(data['created_at']))
41
+ end
42
+ end
43
+
44
+ def remove_assignment(experiment, _subject)
45
+ remove(experiment.handle.to_s, KEY)
46
+ end
47
+
48
+ def retrieve_start_timestamp(_experiment)
49
+ nil
50
+ end
51
+
52
+ def store_start_timestamp(_experiment, _timestamp)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ protected
57
+
58
+ def get(scope, key)
59
+ ensure_cookies_is_set
60
+ cookies[scope_key(scope, key)]
61
+ end
62
+
63
+ def set(scope, key, value)
64
+ ensure_cookies_is_set
65
+ cookies[scope_key(scope, key)] = {
66
+ value: value,
67
+ expires: Time.now.utc.advance(seconds: cookie_lifespan),
68
+ }
69
+ end
70
+
71
+ def remove(scope, key)
72
+ ensure_cookies_is_set
73
+ cookies.delete(scope_key(scope, key))
74
+ end
75
+
76
+ private
77
+
78
+ def ensure_cookies_is_set
79
+ raise Verdict::StorageError, 'cookies must be an instance of ActionDispatch::Cookies::CookieJar' if cookies.nil?
80
+ end
81
+
82
+ def digest_of(value)
83
+ Digest::MD5.hexdigest(value)
84
+ end
85
+
86
+ def find_group_by_digest(experiment, digest)
87
+ experiment.groups.values.find do |group|
88
+ digest_of(group.to_s) == digest
89
+ end
90
+ end
91
+
92
+ def parse_cookie_value(value)
93
+ value = value[:value] if value.is_a?(Hash)
94
+
95
+ JSON.parse(value)
96
+ rescue JSON::ParserError, TypeError
97
+ {}
98
+ end
99
+
100
+ def scope_key(scope, key)
101
+ "#{digest_of(scope)}_#{key}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.12.0"
2
+ VERSION = "0.13.0"
3
3
  end
@@ -414,6 +414,16 @@ class ExperimentTest < Minitest::Test
414
414
  assert e.started?, "The experiment should have started after the first assignment"
415
415
  end
416
416
 
417
+ def test_experiment_set_start_timestamp_handles_storage_that_does_not_implement_timestamps
418
+ e = Verdict::Experiment.new('starting_test') do
419
+ groups { group :all, 100 }
420
+ end
421
+
422
+ e.storage.expects(:store_start_timestamp).raises(NotImplementedError)
423
+
424
+ assert_nil e.send(:set_start_timestamp)
425
+ end
426
+
417
427
  def test_no_storage
418
428
  e = Verdict::Experiment.new('starting_test') do
419
429
  groups { group :all, 100 }
@@ -460,6 +470,101 @@ class ExperimentTest < Minitest::Test
460
470
  end
461
471
  end
462
472
 
473
+ def test_is_scheduled
474
+ e = Verdict::Experiment.new(:json) do
475
+ groups do
476
+ group :a, :half
477
+ group :b, :rest
478
+ end
479
+ schedule_start_timestamp Time.new(2020, 1, 1)
480
+ schedule_end_timestamp Time.new(2020, 1, 3)
481
+ end
482
+
483
+ # Within the interval
484
+ Timecop.freeze(Time.new(2020, 1, 2)) do
485
+ assert e.send(:is_scheduled?)
486
+ end
487
+ # Too early
488
+ Timecop.freeze(Time.new(2019, 12, 30)) do
489
+ assert !e.send(:is_scheduled?)
490
+ end
491
+ # Too late
492
+ Timecop.freeze(Time.new(2020, 1, 4)) do
493
+ assert !e.send(:is_scheduled?)
494
+ end
495
+ end
496
+
497
+ def test_is_scheduled_no_end_timestamp
498
+ e = Verdict::Experiment.new(:json) do
499
+ groups do
500
+ group :a, :half
501
+ group :b, :rest
502
+ end
503
+ schedule_start_timestamp Time.new(2020, 1, 1)
504
+ end
505
+
506
+ # Within the interval because there is no end date
507
+ Timecop.freeze(Time.new(2030, 1, 1)) do
508
+ assert e.send(:is_scheduled?)
509
+ end
510
+ # Too early
511
+ Timecop.freeze(Time.new(2019, 12, 30)) do
512
+ assert !e.send(:is_scheduled?)
513
+ end
514
+ end
515
+
516
+ def test_is_scheduled_no_start_timestamp
517
+ e = Verdict::Experiment.new(:json) do
518
+ groups do
519
+ group :a, :half
520
+ group :b, :rest
521
+ end
522
+ schedule_end_timestamp Time.new(2020, 1, 3)
523
+ end
524
+
525
+ # Within the interval because there is no start date
526
+ Timecop.freeze(Time.new(2019, 12, 30)) do
527
+ assert e.send(:is_scheduled?)
528
+ end
529
+ # Too late
530
+ Timecop.freeze(Time.new(2020, 1, 4)) do
531
+ assert !e.send(:is_scheduled?)
532
+ end
533
+ end
534
+
535
+ def test_switch_respects_time_schedule
536
+ e = Verdict::Experiment.new('test') do
537
+ groups do
538
+ group :a, :half
539
+ group :b, :rest
540
+ end
541
+ schedule_start_timestamp Time.new(2020, 1, 1)
542
+ schedule_end_timestamp Time.new(2020, 1, 2)
543
+ end
544
+
545
+ Timecop.freeze(Time.new(2020, 1, 3)) do
546
+ assert_nil e.switch(1)
547
+ end
548
+ end
549
+
550
+ def test_switch_respects_time_schedule_even_after_assignment
551
+ e = Verdict::Experiment.new('test') do
552
+ groups do
553
+ group :a, :half
554
+ group :b, :rest
555
+ end
556
+ end
557
+
558
+ assert_equal :a, e.switch(1)
559
+
560
+ e.schedule_start_timestamp Time.new(2020, 1, 1)
561
+ e.schedule_end_timestamp Time.new(2020, 1, 2)
562
+
563
+ Timecop.freeze(Time.new(2020, 1, 3)) do
564
+ assert_nil e.switch(1)
565
+ end
566
+ end
567
+
463
568
  private
464
569
 
465
570
  def redis
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'fake_app'
5
+
6
+ class CookieStorageTest < Minitest::Test
7
+ def setup
8
+ @storage = Verdict::Storage::CookieStorage.new.tap do |s|
9
+ s.cookies = ActionDispatch::Cookies::CookieJar.new(nil)
10
+ end
11
+ @experiment = Verdict::Experiment.new(:cookie_storage_test) do
12
+ groups { group :all, 100 }
13
+ storage @storage, store_unqualified: true
14
+ end
15
+ @subject = stub(id: 'bob')
16
+ @assignment = Verdict::Assignment.new(@experiment, @subject, @experiment.group(:all), nil)
17
+ end
18
+
19
+ def test_cookie_lifespan_has_a_default
20
+ cookie_lifespan = Verdict::Storage::CookieStorage.new.cookie_lifespan
21
+
22
+ assert_equal Verdict::Storage::CookieStorage::DEFAULT_COOKIE_LIFESPAN_SECONDS, cookie_lifespan
23
+ end
24
+
25
+ def test_cookie_lifespan_can_be_configured
26
+ storage = Verdict::Storage::CookieStorage.new(cookie_lifespan: 60)
27
+
28
+ assert_equal 60, storage.cookie_lifespan
29
+ end
30
+
31
+ def test_raises_storage_error_when_cookies_is_nil
32
+ storage = Verdict::Storage::CookieStorage.new
33
+
34
+ assert_raises(Verdict::StorageError) { storage.store_assignment(@assignment) }
35
+ assert_raises(Verdict::StorageError) { storage.retrieve_assignment(@experiment, @subject) }
36
+ assert_raises(Verdict::StorageError) { storage.remove_assignment(@experiment, @subject) }
37
+ end
38
+
39
+ def test_store_assignment_returns_true_when_an_assignment_is_stored
40
+ assert @storage.store_assignment(@assignment)
41
+ refute_nil @storage.retrieve_assignment(@experiment, @subject)
42
+ end
43
+
44
+ def test_retrieve_assignment_returns_an_assignment
45
+ @storage.store_assignment(@assignment)
46
+ assignment = @storage.retrieve_assignment(@experiment, @subject)
47
+
48
+ assert assignment.returning?
49
+ assert_equal :all, assignment.handle.to_sym
50
+ assert_equal @experiment, assignment.experiment
51
+ assert_equal @subject, assignment.subject
52
+ end
53
+
54
+ def test_retrieve_assignment_returns_nil_when_an_assignment_does_not_exist
55
+ assert_nil @storage.retrieve_assignment(@experiment, @subject)
56
+ end
57
+
58
+ def test_retrieve_assignment_returns_nil_when_the_assignment_group_is_invalid
59
+ invalid_group = Verdict::Group.new(@experiment, :invalid)
60
+ assignment = Verdict::Assignment.new(@experiment, @subject, invalid_group, nil)
61
+
62
+ @storage.store_assignment(assignment)
63
+
64
+ assert_nil @storage.retrieve_assignment(@experiment, @subject)
65
+ end
66
+
67
+ def test_remove_assignment_returns_true_when_removing_an_assignment
68
+ @storage.store_assignment(@assignment)
69
+
70
+ assert @storage.remove_assignment(@experiment, nil)
71
+ assert_nil @storage.retrieve_assignment(@experiment, @subject)
72
+ end
73
+
74
+ def test_retrieve_start_timestamp_always_returns_nil
75
+ assert_nil @storage.retrieve_start_timestamp(nil)
76
+ end
77
+
78
+ def test_store_start_timestamp_raises_not_implemented_error
79
+ assert_raises(NotImplementedError) { @storage.store_start_timestamp(nil, nil) }
80
+ end
81
+ end
@@ -17,6 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
+ gem.metadata["allowed_push_host"] = "https://rubygems.org"
21
+
20
22
  gem.add_development_dependency("minitest", '~> 5.2')
21
23
  gem.add_development_dependency("rake")
22
24
  gem.add_development_dependency("mocha")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verdict
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-11 00:00:00.000000000 Z
11
+ date: 2020-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -106,10 +106,12 @@ files:
106
106
  - ".gitignore"
107
107
  - ".travis.yml"
108
108
  - CHANGELOG.md
109
+ - CONTRIBUTING.md
109
110
  - Gemfile
110
111
  - LICENSE
111
112
  - README.md
112
113
  - Rakefile
114
+ - docs/concepts.md
113
115
  - lib/verdict.rb
114
116
  - lib/verdict/assignment.rb
115
117
  - lib/verdict/conversion.rb
@@ -126,6 +128,7 @@ files:
126
128
  - lib/verdict/segmenters/static_segmenter.rb
127
129
  - lib/verdict/storage.rb
128
130
  - lib/verdict/storage/base_storage.rb
131
+ - lib/verdict/storage/cookie_storage.rb
129
132
  - lib/verdict/storage/legacy_redis_storage.rb
130
133
  - lib/verdict/storage/memory_storage.rb
131
134
  - lib/verdict/storage/mock_storage.rb
@@ -148,6 +151,7 @@ files:
148
151
  - test/segmenters/rollout_segmenter_test.rb
149
152
  - test/segmenters/static_segmenter_test.rb
150
153
  - test/storage/base_storage_test.rb
154
+ - test/storage/cookie_storage_test.rb
151
155
  - test/storage/legacy_redis_storage_test.rb
152
156
  - test/storage/memory_storage_test.rb
153
157
  - test/storage/redis_storage_test.rb
@@ -156,7 +160,8 @@ files:
156
160
  - verdict.gemspec
157
161
  homepage: http://github.com/Shopify/verdict
158
162
  licenses: []
159
- metadata: {}
163
+ metadata:
164
+ allowed_push_host: https://rubygems.org
160
165
  post_install_message:
161
166
  rdoc_options: []
162
167
  require_paths:
@@ -193,6 +198,7 @@ test_files:
193
198
  - test/segmenters/rollout_segmenter_test.rb
194
199
  - test/segmenters/static_segmenter_test.rb
195
200
  - test/storage/base_storage_test.rb
201
+ - test/storage/cookie_storage_test.rb
196
202
  - test/storage/legacy_redis_storage_test.rb
197
203
  - test/storage/memory_storage_test.rb
198
204
  - test/storage/redis_storage_test.rb