split 1.1.0 → 1.2.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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +11 -0
 - data/README.md +31 -10
 - data/lib/split.rb +1 -1
 - data/lib/split/configuration.rb +4 -0
 - data/lib/split/experiment.rb +29 -1
 - data/lib/split/trial.rb +9 -5
 - data/lib/split/version.rb +1 -1
 - data/spec/configuration_spec.rb +31 -0
 - data/spec/experiment_spec.rb +23 -0
 - data/spec/trial_spec.rb +57 -0
 - metadata +2 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA1:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: acc9cc61f2fbaacd23c4101ce3533baad6fc2f5e
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 0f1608e29221a58215ee240ac01ac921256e8466
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 5e4cd8b3e649929c5c3a9f406a8a587c7cc23121146f0c0002ff05d6c86b7c61e508a05acab5d70172864d73f4c544263e72d1476a8d5afe134deb34b6012172
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: f9be6dc468f3fe7941e07b3734d1fae28a4a05209501cb093769d8f14001ec596d5b3627f808444523b303fe4d44de0946a328e04a1b9afe64434ba5c54c0493
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -1,3 +1,14 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            ## 1.2.0 (January 24th, 2015)
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Features
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              - Configure redis using environment variables if available (@saratovsource , #293)
         
     | 
| 
      
 6 
     | 
    
         
            +
              - Store metadata on experiment configuration (@dekz, #291)
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            Bugfixes:
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
             - Revert the Trial#complete! public API to support noargs (@dekz, #292)
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
       1 
12 
     | 
    
         
             
            ## 1.1.0 (January 9th, 2015)
         
     | 
| 
       2 
13 
     | 
    
         | 
| 
       3 
14 
     | 
    
         
             
            Features:
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -119,7 +119,7 @@ You can find more examples, tutorials and guides on the [wiki](https://github.co 
     | 
|
| 
       119 
119 
     | 
    
         | 
| 
       120 
120 
     | 
    
         
             
            ## Statistical Validity
         
     | 
| 
       121 
121 
     | 
    
         | 
| 
       122 
     | 
    
         
            -
            Split has two options for you to use to determine which alternative is the best. 
     | 
| 
      
 122 
     | 
    
         
            +
            Split has two options for you to use to determine which alternative is the best.
         
     | 
| 
       123 
123 
     | 
    
         | 
| 
       124 
124 
     | 
    
         
             
            The first option (default on the dashboard) uses a z test (n>30) for the difference between your control and alternative conversion rates to calculate statistical significance. This test will tell you whether an alternative is better or worse than your control, but it will not distinguish between which alternative is the best in an experiment with multiple alternatives. Split will only tell you if your experiment is 90%, 95%, or 99% significant, and this test only works if you have more than 30 participants and 5 conversions for each branch.
         
     | 
| 
       125 
125 
     | 
    
         | 
| 
         @@ -364,6 +364,8 @@ Split.configure do |config| 
     | 
|
| 
       364 
364 
     | 
    
         
             
            end
         
     | 
| 
       365 
365 
     | 
    
         
             
            ```
         
     | 
| 
       366 
366 
     | 
    
         | 
| 
      
 367 
     | 
    
         
            +
            You can set different Redis host via environment variable ```REDIS_URL```.
         
     | 
| 
      
 368 
     | 
    
         
            +
             
     | 
| 
       367 
369 
     | 
    
         
             
            ### Filtering
         
     | 
| 
       368 
370 
     | 
    
         | 
| 
       369 
371 
     | 
    
         
             
            In most scenarios you don't want to have AB-Testing enabled for web spiders, robots or special groups of users.
         
     | 
| 
         @@ -443,6 +445,28 @@ and: 
     | 
|
| 
       443 
445 
     | 
    
         
             
            finished("my_first_experiment")
         
     | 
| 
       444 
446 
     | 
    
         
             
            ```
         
     | 
| 
       445 
447 
     | 
    
         | 
| 
      
 448 
     | 
    
         
            +
            You can also add meta data for each experiment, very useful when you need more than an alternative name to change behaviour:
         
     | 
| 
      
 449 
     | 
    
         
            +
             
     | 
| 
      
 450 
     | 
    
         
            +
            ```yaml
         
     | 
| 
      
 451 
     | 
    
         
            +
            my_first_experiment:
         
     | 
| 
      
 452 
     | 
    
         
            +
              alternatives:
         
     | 
| 
      
 453 
     | 
    
         
            +
                - a
         
     | 
| 
      
 454 
     | 
    
         
            +
                - b
         
     | 
| 
      
 455 
     | 
    
         
            +
                meta:
         
     | 
| 
      
 456 
     | 
    
         
            +
                  a:
         
     | 
| 
      
 457 
     | 
    
         
            +
                    text: "Have a fantastic day"
         
     | 
| 
      
 458 
     | 
    
         
            +
                    b:
         
     | 
| 
      
 459 
     | 
    
         
            +
                      text: "Don't get hit by a bus"
         
     | 
| 
      
 460 
     | 
    
         
            +
            ```
         
     | 
| 
      
 461 
     | 
    
         
            +
             
     | 
| 
      
 462 
     | 
    
         
            +
            This allows for some advanced experiment configuration using methods like:
         
     | 
| 
      
 463 
     | 
    
         
            +
             
     | 
| 
      
 464 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 465 
     | 
    
         
            +
            trial.alternative.name # => "a"
         
     | 
| 
      
 466 
     | 
    
         
            +
             
     | 
| 
      
 467 
     | 
    
         
            +
            trial.metadata['text'] # => "Have a fantastic day"
         
     | 
| 
      
 468 
     | 
    
         
            +
            ```
         
     | 
| 
      
 469 
     | 
    
         
            +
             
     | 
| 
       446 
470 
     | 
    
         
             
            #### Metrics
         
     | 
| 
       447 
471 
     | 
    
         | 
| 
       448 
472 
     | 
    
         
             
            You might wish to track generic metrics, such as conversions, and use
         
     | 
| 
         @@ -503,7 +527,7 @@ To complete a goal conversion, you do it like: 
     | 
|
| 
       503 
527 
     | 
    
         
             
            finished("link_color" => "purchase")
         
     | 
| 
       504 
528 
     | 
    
         
             
            ```
         
     | 
| 
       505 
529 
     | 
    
         | 
| 
       506 
     | 
    
         
            -
            **NOTE:** This does not mean that a single experiment can have/complete progressive goals. 
     | 
| 
      
 530 
     | 
    
         
            +
            **NOTE:** This does not mean that a single experiment can have/complete progressive goals.
         
     | 
| 
       507 
531 
     | 
    
         | 
| 
       508 
532 
     | 
    
         
             
            **Good Example**: Test if listing Plan A first result in more conversions to Plan A (goal: "plana_conversion") or Plan B (goal: "planb_conversion").
         
     | 
| 
       509 
533 
     | 
    
         | 
| 
         @@ -549,11 +573,8 @@ production: redis1.example.com:6379 
     | 
|
| 
       549 
573 
     | 
    
         
             
            And our initializer:
         
     | 
| 
       550 
574 
     | 
    
         | 
| 
       551 
575 
     | 
    
         
             
            ```ruby
         
     | 
| 
       552 
     | 
    
         
            -
             
     | 
| 
       553 
     | 
    
         
            -
             
     | 
| 
       554 
     | 
    
         
            -
             
     | 
| 
       555 
     | 
    
         
            -
            split_config = YAML.load_file(rails_root + '/config/split.yml')
         
     | 
| 
       556 
     | 
    
         
            -
            Split.redis = split_config[rails_env]
         
     | 
| 
      
 576 
     | 
    
         
            +
            split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
         
     | 
| 
      
 577 
     | 
    
         
            +
            Split.redis = split_config[Rails.env]
         
     | 
| 
       557 
578 
     | 
    
         
             
            ```
         
     | 
| 
       558 
579 
     | 
    
         | 
| 
       559 
580 
     | 
    
         
             
            ## Namespaces
         
     | 
| 
         @@ -601,11 +622,11 @@ end 
     | 
|
| 
       601 
622 
     | 
    
         | 
| 
       602 
623 
     | 
    
         
             
            ## Algorithms
         
     | 
| 
       603 
624 
     | 
    
         | 
| 
       604 
     | 
    
         
            -
            By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test. 
     | 
| 
      
 625 
     | 
    
         
            +
            By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test.
         
     | 
| 
       605 
626 
     | 
    
         
             
            It is possible to specify static weights to favor certain alternatives.
         
     | 
| 
       606 
627 
     | 
    
         | 
| 
       607 
     | 
    
         
            -
            `Split::Algorithms::Whiplash` is an implementation of a [multi-armed bandit algorithm](http://stevehanov.ca/blog/index.php?id=132). 
     | 
| 
       608 
     | 
    
         
            -
            This algorithm will automatically weight the alternatives based on their relative performance, 
     | 
| 
      
 628 
     | 
    
         
            +
            `Split::Algorithms::Whiplash` is an implementation of a [multi-armed bandit algorithm](http://stevehanov.ca/blog/index.php?id=132).
         
     | 
| 
      
 629 
     | 
    
         
            +
            This algorithm will automatically weight the alternatives based on their relative performance,
         
     | 
| 
       609 
630 
     | 
    
         
             
            choosing the better-performing ones more often as trials are completed.
         
     | 
| 
       610 
631 
     | 
    
         | 
| 
       611 
632 
     | 
    
         
             
            Users may also write their own algorithms. The default algorithm may be specified globally in the configuration file, or on a per experiment basis using the experiments hash of the configuration file.
         
     | 
    
        data/lib/split.rb
    CHANGED
    
    
    
        data/lib/split/configuration.rb
    CHANGED
    
    | 
         @@ -141,6 +141,10 @@ module Split 
     | 
|
| 
       141 
141 
     | 
    
         
             
                        experiment_config[experiment_name.to_sym][:goals] = goals
         
     | 
| 
       142 
142 
     | 
    
         
             
                      end
         
     | 
| 
       143 
143 
     | 
    
         | 
| 
      
 144 
     | 
    
         
            +
                      if metadata = value_for(settings, :metadata)
         
     | 
| 
      
 145 
     | 
    
         
            +
                        experiment_config[experiment_name.to_sym][:metadata] = metadata
         
     | 
| 
      
 146 
     | 
    
         
            +
                      end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
       144 
148 
     | 
    
         
             
                      if (resettable = value_for(settings, :resettable)) != nil
         
     | 
| 
       145 
149 
     | 
    
         
             
                        experiment_config[experiment_name.to_sym][:resettable] = resettable
         
     | 
| 
       146 
150 
     | 
    
         
             
                      end
         
     | 
    
        data/lib/split/experiment.rb
    CHANGED
    
    | 
         @@ -6,6 +6,7 @@ module Split 
     | 
|
| 
       6 
6 
     | 
    
         
             
                attr_accessor :goals
         
     | 
| 
       7 
7 
     | 
    
         
             
                attr_accessor :alternatives
         
     | 
| 
       8 
8 
     | 
    
         
             
                attr_accessor :alternative_probabilities
         
     | 
| 
      
 9 
     | 
    
         
            +
                attr_accessor :metadata
         
     | 
| 
       9 
10 
     | 
    
         | 
| 
       10 
11 
     | 
    
         
             
                DEFAULT_OPTIONS = {
         
     | 
| 
       11 
12 
     | 
    
         
             
                  :resettable => true
         
     | 
| 
         @@ -22,6 +23,7 @@ module Split 
     | 
|
| 
       22 
23 
     | 
    
         
             
                    set_alternatives_and_options(
         
     | 
| 
       23 
24 
     | 
    
         
             
                      alternatives: load_alternatives_from_configuration,
         
     | 
| 
       24 
25 
     | 
    
         
             
                      goals: load_goals_from_configuration,
         
     | 
| 
      
 26 
     | 
    
         
            +
                      metadata: load_metadata_from_configuration,
         
     | 
| 
       25 
27 
     | 
    
         
             
                      resettable: exp_config[:resettable],
         
     | 
| 
       26 
28 
     | 
    
         
             
                      algorithm: exp_config[:algorithm]
         
     | 
| 
       27 
29 
     | 
    
         
             
                    )
         
     | 
| 
         @@ -29,6 +31,7 @@ module Split 
     | 
|
| 
       29 
31 
     | 
    
         
             
                    set_alternatives_and_options(
         
     | 
| 
       30 
32 
     | 
    
         
             
                      alternatives: alternatives,
         
     | 
| 
       31 
33 
     | 
    
         
             
                      goals: options[:goals],
         
     | 
| 
      
 34 
     | 
    
         
            +
                      metadata: options[:metadata],
         
     | 
| 
       32 
35 
     | 
    
         
             
                      resettable: options[:resettable],
         
     | 
| 
       33 
36 
     | 
    
         
             
                      algorithm: options[:algorithm]
         
     | 
| 
       34 
37 
     | 
    
         
             
                    )
         
     | 
| 
         @@ -40,6 +43,7 @@ module Split 
     | 
|
| 
       40 
43 
     | 
    
         
             
                  self.goals = options[:goals]
         
     | 
| 
       41 
44 
     | 
    
         
             
                  self.resettable = options[:resettable]
         
     | 
| 
       42 
45 
     | 
    
         
             
                  self.algorithm = options[:algorithm]
         
     | 
| 
      
 46 
     | 
    
         
            +
                  self.metadata = options[:metadata]
         
     | 
| 
       43 
47 
     | 
    
         
             
                end
         
     | 
| 
       44 
48 
     | 
    
         | 
| 
       45 
49 
     | 
    
         
             
                def extract_alternatives_from_options(options)
         
     | 
| 
         @@ -56,6 +60,7 @@ module Split 
     | 
|
| 
       56 
60 
     | 
    
         
             
                    if exp_config
         
     | 
| 
       57 
61 
     | 
    
         
             
                      alts = load_alternatives_from_configuration
         
     | 
| 
       58 
62 
     | 
    
         
             
                      options[:goals] = load_goals_from_configuration
         
     | 
| 
      
 63 
     | 
    
         
            +
                      options[:metadata] = load_metadata_from_configuration
         
     | 
| 
       59 
64 
     | 
    
         
             
                      options[:resettable] = exp_config[:resettable]
         
     | 
| 
       60 
65 
     | 
    
         
             
                      options[:algorithm] = exp_config[:algorithm]
         
     | 
| 
       61 
66 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -79,16 +84,20 @@ module Split 
     | 
|
| 
       79 
84 
     | 
    
         
             
                    start unless Split.configuration.start_manually
         
     | 
| 
       80 
85 
     | 
    
         
             
                    @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
         
     | 
| 
       81 
86 
     | 
    
         
             
                    @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
         
     | 
| 
      
 87 
     | 
    
         
            +
                    Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
         
     | 
| 
       82 
88 
     | 
    
         
             
                  else
         
     | 
| 
       83 
89 
     | 
    
         
             
                    existing_alternatives = load_alternatives_from_redis
         
     | 
| 
       84 
90 
     | 
    
         
             
                    existing_goals = load_goals_from_redis
         
     | 
| 
       85 
     | 
    
         
            -
                     
     | 
| 
      
 91 
     | 
    
         
            +
                    existing_metadata = load_metadata_from_redis
         
     | 
| 
      
 92 
     | 
    
         
            +
                    unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
         
     | 
| 
       86 
93 
     | 
    
         
             
                      reset
         
     | 
| 
       87 
94 
     | 
    
         
             
                      @alternatives.each(&:delete)
         
     | 
| 
       88 
95 
     | 
    
         
             
                      delete_goals
         
     | 
| 
      
 96 
     | 
    
         
            +
                      delete_metadata
         
     | 
| 
       89 
97 
     | 
    
         
             
                      Split.redis.del(@name)
         
     | 
| 
       90 
98 
     | 
    
         
             
                      @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
         
     | 
| 
       91 
99 
     | 
    
         
             
                      @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
         
     | 
| 
      
 100 
     | 
    
         
            +
                      Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
         
     | 
| 
       92 
101 
     | 
    
         
             
                    end
         
     | 
| 
       93 
102 
     | 
    
         
             
                  end
         
     | 
| 
       94 
103 
     | 
    
         | 
| 
         @@ -221,6 +230,10 @@ module Split 
     | 
|
| 
       221 
230 
     | 
    
         
             
                  "#{key}:finished"
         
     | 
| 
       222 
231 
     | 
    
         
             
                end
         
     | 
| 
       223 
232 
     | 
    
         | 
| 
      
 233 
     | 
    
         
            +
                def metadata_key
         
     | 
| 
      
 234 
     | 
    
         
            +
                  "#{name}:metadata"
         
     | 
| 
      
 235 
     | 
    
         
            +
                end
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
       224 
237 
     | 
    
         
             
                def resettable?
         
     | 
| 
       225 
238 
     | 
    
         
             
                  resettable
         
     | 
| 
       226 
239 
     | 
    
         
             
                end
         
     | 
| 
         @@ -238,6 +251,7 @@ module Split 
     | 
|
| 
       238 
251 
     | 
    
         
             
                  Split.redis.srem(:experiments, name)
         
     | 
| 
       239 
252 
     | 
    
         
             
                  Split.redis.del(name)
         
     | 
| 
       240 
253 
     | 
    
         
             
                  delete_goals
         
     | 
| 
      
 254 
     | 
    
         
            +
                  delete_metadata
         
     | 
| 
       241 
255 
     | 
    
         
             
                  Split.configuration.on_experiment_delete.call(self)
         
     | 
| 
       242 
256 
     | 
    
         
             
                  increment_version
         
     | 
| 
       243 
257 
     | 
    
         
             
                end
         
     | 
| 
         @@ -246,12 +260,17 @@ module Split 
     | 
|
| 
       246 
260 
     | 
    
         
             
                  Split.redis.del(goals_key)
         
     | 
| 
       247 
261 
     | 
    
         
             
                end
         
     | 
| 
       248 
262 
     | 
    
         | 
| 
      
 263 
     | 
    
         
            +
                def delete_metadata
         
     | 
| 
      
 264 
     | 
    
         
            +
                  Split.redis.del(metadata_key)
         
     | 
| 
      
 265 
     | 
    
         
            +
                end
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
       249 
267 
     | 
    
         
             
                def load_from_redis
         
     | 
| 
       250 
268 
     | 
    
         
             
                  exp_config = Split.redis.hgetall(experiment_config_key)
         
     | 
| 
       251 
269 
     | 
    
         
             
                  self.resettable = exp_config['resettable']
         
     | 
| 
       252 
270 
     | 
    
         
             
                  self.algorithm = exp_config['algorithm']
         
     | 
| 
       253 
271 
     | 
    
         
             
                  self.alternatives = load_alternatives_from_redis
         
     | 
| 
       254 
272 
     | 
    
         
             
                  self.goals = load_goals_from_redis
         
     | 
| 
      
 273 
     | 
    
         
            +
                  self.metadata = load_metadata_from_redis
         
     | 
| 
       255 
274 
     | 
    
         
             
                end
         
     | 
| 
       256 
275 
     | 
    
         | 
| 
       257 
276 
     | 
    
         
             
                def calc_winning_alternatives
         
     | 
| 
         @@ -388,6 +407,10 @@ module Split 
     | 
|
| 
       388 
407 
     | 
    
         
             
                  "experiment_configurations/#{@name}"
         
     | 
| 
       389 
408 
     | 
    
         
             
                end
         
     | 
| 
       390 
409 
     | 
    
         | 
| 
      
 410 
     | 
    
         
            +
                def load_metadata_from_configuration
         
     | 
| 
      
 411 
     | 
    
         
            +
                  metadata = Split.configuration.experiment_for(@name)[:metadata]
         
     | 
| 
      
 412 
     | 
    
         
            +
                end
         
     | 
| 
      
 413 
     | 
    
         
            +
             
     | 
| 
       391 
414 
     | 
    
         
             
                def load_goals_from_configuration
         
     | 
| 
       392 
415 
     | 
    
         
             
                  goals = Split.configuration.experiment_for(@name)[:goals]
         
     | 
| 
       393 
416 
     | 
    
         
             
                  if goals.nil?
         
     | 
| 
         @@ -401,6 +424,11 @@ module Split 
     | 
|
| 
       401 
424 
     | 
    
         
             
                  Split.redis.lrange(goals_key, 0, -1)
         
     | 
| 
       402 
425 
     | 
    
         
             
                end
         
     | 
| 
       403 
426 
     | 
    
         | 
| 
      
 427 
     | 
    
         
            +
                def load_metadata_from_redis
         
     | 
| 
      
 428 
     | 
    
         
            +
                  meta = Split.redis.get(metadata_key)
         
     | 
| 
      
 429 
     | 
    
         
            +
                  JSON.parse(meta) unless meta.nil?
         
     | 
| 
      
 430 
     | 
    
         
            +
                end
         
     | 
| 
      
 431 
     | 
    
         
            +
             
     | 
| 
       404 
432 
     | 
    
         
             
                def load_alternatives_from_configuration
         
     | 
| 
       405 
433 
     | 
    
         
             
                  alts = Split.configuration.experiment_for(@name)[:alternatives]
         
     | 
| 
       406 
434 
     | 
    
         
             
                  raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
         
     | 
    
        data/lib/split/trial.rb
    CHANGED
    
    | 
         @@ -1,10 +1,12 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Split
         
     | 
| 
       2 
2 
     | 
    
         
             
              class Trial
         
     | 
| 
       3 
3 
     | 
    
         
             
                attr_accessor :experiment
         
     | 
| 
      
 4 
     | 
    
         
            +
                attr_accessor :metadata
         
     | 
| 
       4 
5 
     | 
    
         | 
| 
       5 
6 
     | 
    
         
             
                def initialize(attrs = {})
         
     | 
| 
       6 
7 
     | 
    
         
             
                  self.experiment   = attrs.delete(:experiment)
         
     | 
| 
       7 
8 
     | 
    
         
             
                  self.alternative  = attrs.delete(:alternative)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  self.metadata  = attrs.delete(:metadata)
         
     | 
| 
       8 
10 
     | 
    
         | 
| 
       9 
11 
     | 
    
         
             
                  @user             = attrs.delete(:user)
         
     | 
| 
       10 
12 
     | 
    
         
             
                  @options          = attrs
         
     | 
| 
         @@ -12,6 +14,10 @@ module Split 
     | 
|
| 
       12 
14 
     | 
    
         
             
                  @alternative_choosen = false
         
     | 
| 
       13 
15 
     | 
    
         
             
                end
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
      
 17 
     | 
    
         
            +
                def metadata
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @metadata ||= experiment.metadata[alternative.name]
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
       15 
21 
     | 
    
         
             
                def alternative
         
     | 
| 
       16 
22 
     | 
    
         
             
                  @alternative ||=  if @experiment.has_winner?
         
     | 
| 
       17 
23 
     | 
    
         
             
                                      @experiment.winner
         
     | 
| 
         @@ -26,14 +32,12 @@ module Split 
     | 
|
| 
       26 
32 
     | 
    
         
             
                  end
         
     | 
| 
       27 
33 
     | 
    
         
             
                end
         
     | 
| 
       28 
34 
     | 
    
         | 
| 
       29 
     | 
    
         
            -
                def complete!(goals, context = nil)
         
     | 
| 
       30 
     | 
    
         
            -
                  goals = goals || []
         
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
      
 35 
     | 
    
         
            +
                def complete!(goals=[], context = nil)
         
     | 
| 
       32 
36 
     | 
    
         
             
                  if alternative
         
     | 
| 
       33 
     | 
    
         
            -
                    if goals.empty?
         
     | 
| 
      
 37 
     | 
    
         
            +
                    if Array(goals).empty?
         
     | 
| 
       34 
38 
     | 
    
         
             
                      alternative.increment_completion
         
     | 
| 
       35 
39 
     | 
    
         
             
                    else
         
     | 
| 
       36 
     | 
    
         
            -
                      goals.each {|g| alternative.increment_completion(g) }
         
     | 
| 
      
 40 
     | 
    
         
            +
                      Array(goals).each {|g| alternative.increment_completion(g) }
         
     | 
| 
       37 
41 
     | 
    
         
             
                    end
         
     | 
| 
       38 
42 
     | 
    
         | 
| 
       39 
43 
     | 
    
         
             
                    context.send(Split.configuration.on_trial_complete, self) \
         
     | 
    
        data/lib/split/version.rb
    CHANGED
    
    
    
        data/spec/configuration_spec.rb
    CHANGED
    
    | 
         @@ -90,6 +90,36 @@ describe Split::Configuration do 
     | 
|
| 
       90 
90 
     | 
    
         
             
                    end
         
     | 
| 
       91 
91 
     | 
    
         
             
                  end
         
     | 
| 
       92 
92 
     | 
    
         | 
| 
      
 93 
     | 
    
         
            +
                  context "in a configuration with metadata" do
         
     | 
| 
      
 94 
     | 
    
         
            +
                    before do
         
     | 
| 
      
 95 
     | 
    
         
            +
                      experiments_yaml = <<-eos
         
     | 
| 
      
 96 
     | 
    
         
            +
                        my_experiment:
         
     | 
| 
      
 97 
     | 
    
         
            +
                          alternatives:
         
     | 
| 
      
 98 
     | 
    
         
            +
                            - name: Control Opt
         
     | 
| 
      
 99 
     | 
    
         
            +
                              percent: 67
         
     | 
| 
      
 100 
     | 
    
         
            +
                            - name: Alt One
         
     | 
| 
      
 101 
     | 
    
         
            +
                              percent: 10
         
     | 
| 
      
 102 
     | 
    
         
            +
                            - name: Alt Two
         
     | 
| 
      
 103 
     | 
    
         
            +
                              percent: 23
         
     | 
| 
      
 104 
     | 
    
         
            +
                          metadata:
         
     | 
| 
      
 105 
     | 
    
         
            +
                            Control Opt:
         
     | 
| 
      
 106 
     | 
    
         
            +
                              text: 'Control Option'
         
     | 
| 
      
 107 
     | 
    
         
            +
                            Alt One:
         
     | 
| 
      
 108 
     | 
    
         
            +
                              text: 'Alternative One'
         
     | 
| 
      
 109 
     | 
    
         
            +
                            Alt Two:
         
     | 
| 
      
 110 
     | 
    
         
            +
                              text: 'Alternative Two'
         
     | 
| 
      
 111 
     | 
    
         
            +
                          resettable: false
         
     | 
| 
      
 112 
     | 
    
         
            +
                        eos
         
     | 
| 
      
 113 
     | 
    
         
            +
                      @config.experiments = YAML.load(experiments_yaml)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                    it 'should have metadata on the experiment' do
         
     | 
| 
      
 117 
     | 
    
         
            +
                      meta = @config.normalized_experiments[:my_experiment][:metadata]
         
     | 
| 
      
 118 
     | 
    
         
            +
                      expect(meta).to_not be nil
         
     | 
| 
      
 119 
     | 
    
         
            +
                      expect(meta['Control Opt']['text']).to eq('Control Option')
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
                  end
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
       93 
123 
     | 
    
         
             
                  context "in a complex configuration" do
         
     | 
| 
       94 
124 
     | 
    
         
             
                    before do
         
     | 
| 
       95 
125 
     | 
    
         
             
                      experiments_yaml = <<-eos
         
     | 
| 
         @@ -120,6 +150,7 @@ describe Split::Configuration do 
     | 
|
| 
       120 
150 
     | 
    
         
             
                      expect(@config.metrics).not_to be_nil
         
     | 
| 
       121 
151 
     | 
    
         
             
                      expect(@config.metrics.keys).to eq([:my_metric])
         
     | 
| 
       122 
152 
     | 
    
         
             
                    end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
       123 
154 
     | 
    
         
             
                  end
         
     | 
| 
       124 
155 
     | 
    
         
             
                end
         
     | 
| 
       125 
156 
     | 
    
         | 
    
        data/spec/experiment_spec.rb
    CHANGED
    
    | 
         @@ -147,6 +147,29 @@ describe Split::Experiment do 
     | 
|
| 
       147 
147 
     | 
    
         | 
| 
       148 
148 
     | 
    
         
             
                end
         
     | 
| 
       149 
149 
     | 
    
         | 
| 
      
 150 
     | 
    
         
            +
                describe '#metadata' do
         
     | 
| 
      
 151 
     | 
    
         
            +
                  let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
         
     | 
| 
      
 152 
     | 
    
         
            +
                  context 'simple hash' do
         
     | 
| 
      
 153 
     | 
    
         
            +
                    let(:meta) {  { 'basket' => 'a', 'cart' => 'b' } }
         
     | 
| 
      
 154 
     | 
    
         
            +
                    it "should persist metadata in redis" do
         
     | 
| 
      
 155 
     | 
    
         
            +
                      experiment.save
         
     | 
| 
      
 156 
     | 
    
         
            +
                      e = Split::ExperimentCatalog.find('basket_text')
         
     | 
| 
      
 157 
     | 
    
         
            +
                      expect(e).to eq(experiment)
         
     | 
| 
      
 158 
     | 
    
         
            +
                      expect(e.metadata).to eq(meta)
         
     | 
| 
      
 159 
     | 
    
         
            +
                    end
         
     | 
| 
      
 160 
     | 
    
         
            +
                  end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                  context 'nested hash' do
         
     | 
| 
      
 163 
     | 
    
         
            +
                    let(:meta) {  { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
         
     | 
| 
      
 164 
     | 
    
         
            +
                    it "should persist metadata in redis" do
         
     | 
| 
      
 165 
     | 
    
         
            +
                      experiment.save
         
     | 
| 
      
 166 
     | 
    
         
            +
                      e = Split::ExperimentCatalog.find('basket_text')
         
     | 
| 
      
 167 
     | 
    
         
            +
                      expect(e).to eq(experiment)
         
     | 
| 
      
 168 
     | 
    
         
            +
                      expect(e.metadata).to eq(meta)
         
     | 
| 
      
 169 
     | 
    
         
            +
                    end
         
     | 
| 
      
 170 
     | 
    
         
            +
                  end
         
     | 
| 
      
 171 
     | 
    
         
            +
                end
         
     | 
| 
      
 172 
     | 
    
         
            +
             
     | 
| 
       150 
173 
     | 
    
         
             
                it "should persist algorithm in redis" do
         
     | 
| 
       151 
174 
     | 
    
         
             
                  experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
         
     | 
| 
       152 
175 
     | 
    
         
             
                  experiment.save
         
     | 
    
        data/spec/trial_spec.rb
    CHANGED
    
    | 
         @@ -33,6 +33,27 @@ describe Split::Trial do 
     | 
|
| 
       33 
33 
     | 
    
         
             
                end
         
     | 
| 
       34 
34 
     | 
    
         
             
              end
         
     | 
| 
       35 
35 
     | 
    
         | 
| 
      
 36 
     | 
    
         
            +
              describe "metadata" do
         
     | 
| 
      
 37 
     | 
    
         
            +
                let(:alternatives) { ['basket', 'cart'] }
         
     | 
| 
      
 38 
     | 
    
         
            +
                let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
         
     | 
| 
      
 39 
     | 
    
         
            +
                let(:experiment) do
         
     | 
| 
      
 40 
     | 
    
         
            +
                  Split::Experiment.new('basket_text', :alternatives => alternatives, :metadata => metadata).save
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                it 'has metadata on each trial' do
         
     | 
| 
      
 44 
     | 
    
         
            +
                  trial = Split::Trial.new(:experiment => experiment, :user => user, :metadata => metadata['cart'],
         
     | 
| 
      
 45 
     | 
    
         
            +
                                           :override => 'cart')
         
     | 
| 
      
 46 
     | 
    
         
            +
                  expect(trial.metadata).to eq(metadata['cart'])
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                it 'has metadata on each trial from the experiment' do
         
     | 
| 
      
 50 
     | 
    
         
            +
                  trial = Split::Trial.new(:experiment => experiment, :user => user)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  trial.choose!
         
     | 
| 
      
 52 
     | 
    
         
            +
                  expect(trial.metadata).to eq(metadata[trial.alternative.name])
         
     | 
| 
      
 53 
     | 
    
         
            +
                  expect(trial.metadata).to match /#{trial.alternative.name}/
         
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
       36 
57 
     | 
    
         
             
              describe "#choose!" do
         
     | 
| 
       37 
58 
     | 
    
         
             
                def expect_alternative(trial, alternative_name)
         
     | 
| 
       38 
59 
     | 
    
         
             
                  3.times do
         
     | 
| 
         @@ -109,6 +130,42 @@ describe Split::Trial do 
     | 
|
| 
       109 
130 
     | 
    
         
             
                  end
         
     | 
| 
       110 
131 
     | 
    
         
             
                end
         
     | 
| 
       111 
132 
     | 
    
         | 
| 
      
 133 
     | 
    
         
            +
                describe "#complete!" do
         
     | 
| 
      
 134 
     | 
    
         
            +
                  let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
         
     | 
| 
      
 135 
     | 
    
         
            +
                  context 'when there are no goals' do
         
     | 
| 
      
 136 
     | 
    
         
            +
                    it 'should complete the trial' do
         
     | 
| 
      
 137 
     | 
    
         
            +
                      trial.choose!
         
     | 
| 
      
 138 
     | 
    
         
            +
                      old_completed_count = trial.alternative.completed_count
         
     | 
| 
      
 139 
     | 
    
         
            +
                      trial.complete!
         
     | 
| 
      
 140 
     | 
    
         
            +
                      expect(trial.alternative.completed_count).to be(old_completed_count+1)
         
     | 
| 
      
 141 
     | 
    
         
            +
                    end
         
     | 
| 
      
 142 
     | 
    
         
            +
                  end
         
     | 
| 
      
 143 
     | 
    
         
            +
             
     | 
| 
      
 144 
     | 
    
         
            +
                  context 'when there are many goals' do
         
     | 
| 
      
 145 
     | 
    
         
            +
                    let(:goals) { ['first', 'second'] }
         
     | 
| 
      
 146 
     | 
    
         
            +
                    let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
         
     | 
| 
      
 147 
     | 
    
         
            +
                    shared_examples_for "goal completion" do
         
     | 
| 
      
 148 
     | 
    
         
            +
                      it 'should not complete the trial' do
         
     | 
| 
      
 149 
     | 
    
         
            +
                        trial.choose!
         
     | 
| 
      
 150 
     | 
    
         
            +
                        old_completed_count = trial.alternative.completed_count
         
     | 
| 
      
 151 
     | 
    
         
            +
                        trial.complete!(goal)
         
     | 
| 
      
 152 
     | 
    
         
            +
                        expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
         
     | 
| 
      
 153 
     | 
    
         
            +
                      end
         
     | 
| 
      
 154 
     | 
    
         
            +
                    end
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
      
 156 
     | 
    
         
            +
                    describe 'Array of Goals' do
         
     | 
| 
      
 157 
     | 
    
         
            +
                      let(:goal) { [goals.first] }
         
     | 
| 
      
 158 
     | 
    
         
            +
                      it_behaves_like 'goal completion'
         
     | 
| 
      
 159 
     | 
    
         
            +
                    end
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                    describe 'String of Goal' do
         
     | 
| 
      
 162 
     | 
    
         
            +
                      let(:goal) { goals.first }
         
     | 
| 
      
 163 
     | 
    
         
            +
                      it_behaves_like 'goal completion'
         
     | 
| 
      
 164 
     | 
    
         
            +
                    end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                  end
         
     | 
| 
      
 167 
     | 
    
         
            +
                end
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
       112 
169 
     | 
    
         
             
                describe "alternative recording" do
         
     | 
| 
       113 
170 
     | 
    
         
             
                  before(:each) { Split.configuration.store_override = false }
         
     | 
| 
       114 
171 
     | 
    
         | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: split
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.2.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Andrew Nesbitt
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire: 
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2015-01- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2015-01-24 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: redis
         
     |