gitlab-experiment 0.5.4 → 0.6.4
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/README.md +24 -0
 - data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +39 -17
 - data/lib/gitlab/experiment.rb +26 -1
 - data/lib/gitlab/experiment/base_interface.rb +8 -1
 - data/lib/gitlab/experiment/configuration.rb +30 -15
 - data/lib/gitlab/experiment/context.rb +14 -6
 - data/lib/gitlab/experiment/cookies.rb +1 -1
 - data/lib/gitlab/experiment/dsl.rb +4 -0
 - data/lib/gitlab/experiment/engine.rb +34 -9
 - data/lib/gitlab/experiment/errors.rb +8 -0
 - data/lib/gitlab/experiment/middleware.rb +27 -0
 - data/lib/gitlab/experiment/rollout.rb +8 -1
 - data/lib/gitlab/experiment/rollout/percent.rb +47 -0
 - data/lib/gitlab/experiment/rspec.rb +1 -1
 - data/lib/gitlab/experiment/variant.rb +5 -1
 - data/lib/gitlab/experiment/version.rb +1 -1
 - metadata +35 -18
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: ed3c0028563936ce8a3fbdfd1de9d64e499102c4ec71a21946b63c56cc2bebd5
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 43389ef9b7b2a4b3e60b82b5ffefbe3635739cb9fafec21dd5e9d59586f942c2
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 30d0988d199f34eb60e3d56d8e7aec3a00222a5de482410c0294e15ce37a2a9dccbb2fb35b372c90b4425879938bec0d7e5b0ffadf5d842ee31abb0ea673aaeb
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 3665443e1d937b77e12debaf3195423b3fad171b651573e58a328dfb5c1f618eb9a282eedcfde68376ae0e7207837b74ae60e4fda616c046caa329b4cc681e68
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -423,6 +423,30 @@ Gitlab::Experiment.configure do |config| 
     | 
|
| 
       423 
423 
     | 
    
         
             
            end
         
     | 
| 
       424 
424 
     | 
    
         
             
            ```
         
     | 
| 
       425 
425 
     | 
    
         | 
| 
      
 426 
     | 
    
         
            +
            ### Middleware
         
     | 
| 
      
 427 
     | 
    
         
            +
             
     | 
| 
      
 428 
     | 
    
         
            +
            There are times when you'll need to do link tracking in email templates, or markdown content -- or other places you won't be able to implement tracking. For these cases, gitlab-experiment comes with middleware that will redirect to a given URL while also tracking that the URL was visited.
         
     | 
| 
      
 429 
     | 
    
         
            +
             
     | 
| 
      
 430 
     | 
    
         
            +
            In Rails this middleware is mounted automatically, with a base path of what's been configured for `mount_at`. If this path is empty the middleware won't be mounted at all.
         
     | 
| 
      
 431 
     | 
    
         
            +
             
     | 
| 
      
 432 
     | 
    
         
            +
            Once mounted, the redirect URLs can be generated using the Rails route helpers. If not using Rails, mount the middleware and generate these URLs yourself.
         
     | 
| 
      
 433 
     | 
    
         
            +
             
     | 
| 
      
 434 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 435 
     | 
    
         
            +
            Gitlab::Experiment.configure do |config|
         
     | 
| 
      
 436 
     | 
    
         
            +
              config.mount_at = '/experiment'
         
     | 
| 
      
 437 
     | 
    
         
            +
            end
         
     | 
| 
      
 438 
     | 
    
         
            +
             
     | 
| 
      
 439 
     | 
    
         
            +
            ex = experiment(:example, foo: :bar)
         
     | 
| 
      
 440 
     | 
    
         
            +
             
     | 
| 
      
 441 
     | 
    
         
            +
            # using rails path/url helpers 
         
     | 
| 
      
 442 
     | 
    
         
            +
            experiment_redirect_path(ex, 'https//docs.gitlab.com/') # => /experiment/example:[context_key]?https//docs.gitlab.com/
         
     | 
| 
      
 443 
     | 
    
         
            +
             
     | 
| 
      
 444 
     | 
    
         
            +
            # manually
         
     | 
| 
      
 445 
     | 
    
         
            +
            "#{Gitlab::Experiment.configure.mount_at}/#{ex.to_param}?https//docs.gitlab.com/"
         
     | 
| 
      
 446 
     | 
    
         
            +
            ```
         
     | 
| 
      
 447 
     | 
    
         
            +
             
     | 
| 
      
 448 
     | 
    
         
            +
            URLS that match the base path will be handled by the middleware and will redirect to the provided redirect path.
         
     | 
| 
      
 449 
     | 
    
         
            +
             
     | 
| 
       426 
450 
     | 
    
         
             
            ## Testing (rspec support)
         
     | 
| 
       427 
451 
     | 
    
         | 
| 
       428 
452 
     | 
    
         
             
            This gem comes with some rspec helpers and custom matchers. These are in flux at the time of writing.
         
     | 
| 
         @@ -18,8 +18,10 @@ Gitlab::Experiment.configure do |config| 
     | 
|
| 
       18 
18 
     | 
    
         
             
              # The domain to use on cookies.
         
     | 
| 
       19 
19 
     | 
    
         
             
              #
         
     | 
| 
       20 
20 
     | 
    
         
             
              # When not set, it uses the current host. If you want to provide specific
         
     | 
| 
       21 
     | 
    
         
            -
              # hosts, you use `:all`, or provide an array 
     | 
| 
       22 
     | 
    
         
            -
              # 
     | 
| 
      
 21 
     | 
    
         
            +
              # hosts, you use `:all`, or provide an array.
         
     | 
| 
      
 22 
     | 
    
         
            +
              #
         
     | 
| 
      
 23 
     | 
    
         
            +
              # Examples:
         
     | 
| 
      
 24 
     | 
    
         
            +
              #   nil, :all, or ['www.gitlab.com', '.gitlab.com']
         
     | 
| 
       23 
25 
     | 
    
         
             
              config.cookie_domain = :all
         
     | 
| 
       24 
26 
     | 
    
         | 
| 
       25 
27 
     | 
    
         
             
              # The default rollout strategy that works for single and multi-variants.
         
     | 
| 
         @@ -28,8 +30,41 @@ Gitlab::Experiment.configure do |config| 
     | 
|
| 
       28 
30 
     | 
    
         
             
              # experiment.
         
     | 
| 
       29 
31 
     | 
    
         
             
              #
         
     | 
| 
       30 
32 
     | 
    
         
             
              # Examples include:
         
     | 
| 
       31 
     | 
    
         
            -
              #   Rollout:: 
     | 
| 
       32 
     | 
    
         
            -
              config.default_rollout = Gitlab::Experiment::Rollout:: 
     | 
| 
      
 33 
     | 
    
         
            +
              #   Rollout::Random, or Rollout::RoundRobin
         
     | 
| 
      
 34 
     | 
    
         
            +
              config.default_rollout = Gitlab::Experiment::Rollout::Percent
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              # Secret seed used in generating context keys.
         
     | 
| 
      
 37 
     | 
    
         
            +
              #
         
     | 
| 
      
 38 
     | 
    
         
            +
              # Consider not using one that's shared with other systems, like Rails'
         
     | 
| 
      
 39 
     | 
    
         
            +
              # SECRET_KEY_BASE. Generate a new secret and utilize that instead.
         
     | 
| 
      
 40 
     | 
    
         
            +
              @context_key_secret = nil
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              # Bit length used by SHA2 in generating context keys.
         
     | 
| 
      
 43 
     | 
    
         
            +
              #
         
     | 
| 
      
 44 
     | 
    
         
            +
              # Using a higher bit length would require more computation time.
         
     | 
| 
      
 45 
     | 
    
         
            +
              #
         
     | 
| 
      
 46 
     | 
    
         
            +
              # Valid bit lengths:
         
     | 
| 
      
 47 
     | 
    
         
            +
              #   256, 384, or 512.
         
     | 
| 
      
 48 
     | 
    
         
            +
              @context_key_bit_length = 256
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
              # The default base path that the middleware (or rails engine) will be
         
     | 
| 
      
 51 
     | 
    
         
            +
              # mounted. Can be nil if you don't want anything to be mounted automatically.
         
     | 
| 
      
 52 
     | 
    
         
            +
              #
         
     | 
| 
      
 53 
     | 
    
         
            +
              # This enables a similar behavior to how links are instrumented in emails.
         
     | 
| 
      
 54 
     | 
    
         
            +
              #
         
     | 
| 
      
 55 
     | 
    
         
            +
              # Examples:
         
     | 
| 
      
 56 
     | 
    
         
            +
              #   '/-/experiment', '/redirect', nil
         
     | 
| 
      
 57 
     | 
    
         
            +
              config.mount_at = '/experiment'
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
              # When using the middleware, links can be instrumented and redirected
         
     | 
| 
      
 60 
     | 
    
         
            +
              # elsewhere. This can be exploited to make a harmful url look innocuous or
         
     | 
| 
      
 61 
     | 
    
         
            +
              # that it's a valid url on your domain. To avoid this, you can provide your
         
     | 
| 
      
 62 
     | 
    
         
            +
              # own logic for what urls will be considered valid and redirected to.
         
     | 
| 
      
 63 
     | 
    
         
            +
              #
         
     | 
| 
      
 64 
     | 
    
         
            +
              # Expected to return a boolean value.
         
     | 
| 
      
 65 
     | 
    
         
            +
              config.redirect_url_validator = lambda do |redirect_url|
         
     | 
| 
      
 66 
     | 
    
         
            +
                true
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
       33 
68 
     | 
    
         | 
| 
       34 
69 
     | 
    
         
             
              # Logic this project uses to determine inclusion in a given experiment.
         
     | 
| 
       35 
70 
     | 
    
         
             
              #
         
     | 
| 
         @@ -80,17 +115,4 @@ Gitlab::Experiment.configure do |config| 
     | 
|
| 
       80 
115 
     | 
    
         
             
                #
         
     | 
| 
       81 
116 
     | 
    
         
             
                # Lograge::Event.log(experiment: name, result: result, signature: signature)
         
     | 
| 
       82 
117 
     | 
    
         
             
              end
         
     | 
| 
       83 
     | 
    
         
            -
             
     | 
| 
       84 
     | 
    
         
            -
              # Algorithm that consistently generates a hash key for a given hash map.
         
     | 
| 
       85 
     | 
    
         
            -
              #
         
     | 
| 
       86 
     | 
    
         
            -
              # Given a specific context hash map, we need to generate a consistent hash
         
     | 
| 
       87 
     | 
    
         
            -
              # key. The logic in here will be used for generating cache keys, and may also
         
     | 
| 
       88 
     | 
    
         
            -
              # be used when determining which variant may be presented.
         
     | 
| 
       89 
     | 
    
         
            -
              #
         
     | 
| 
       90 
     | 
    
         
            -
              # This block is executed within the scope of the experiment and so can access
         
     | 
| 
       91 
     | 
    
         
            -
              # experiment methods, like `name`, `context`, and `signature`.
         
     | 
| 
       92 
     | 
    
         
            -
              config.context_hash_strategy = lambda do |context|
         
     | 
| 
       93 
     | 
    
         
            -
                values = context.values.map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
         
     | 
| 
       94 
     | 
    
         
            -
                Digest::MD5.hexdigest((context.keys + values).join('|'))
         
     | 
| 
       95 
     | 
    
         
            -
              end
         
     | 
| 
       96 
118 
     | 
    
         
             
            end
         
     | 
    
        data/lib/gitlab/experiment.rb
    CHANGED
    
    | 
         @@ -9,6 +9,7 @@ require 'active_support/core_ext/object/blank' 
     | 
|
| 
       9 
9 
     | 
    
         
             
            require 'active_support/core_ext/string/inflections'
         
     | 
| 
       10 
10 
     | 
    
         
             
            require 'active_support/core_ext/module/delegation'
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
            require 'gitlab/experiment/errors'
         
     | 
| 
       12 
13 
     | 
    
         
             
            require 'gitlab/experiment/base_interface'
         
     | 
| 
       13 
14 
     | 
    
         
             
            require 'gitlab/experiment/cache'
         
     | 
| 
       14 
15 
     | 
    
         
             
            require 'gitlab/experiment/callbacks'
         
     | 
| 
         @@ -17,6 +18,7 @@ require 'gitlab/experiment/configuration' 
     | 
|
| 
       17 
18 
     | 
    
         
             
            require 'gitlab/experiment/cookies'
         
     | 
| 
       18 
19 
     | 
    
         
             
            require 'gitlab/experiment/context'
         
     | 
| 
       19 
20 
     | 
    
         
             
            require 'gitlab/experiment/dsl'
         
     | 
| 
      
 21 
     | 
    
         
            +
            require 'gitlab/experiment/middleware'
         
     | 
| 
       20 
22 
     | 
    
         
             
            require 'gitlab/experiment/variant'
         
     | 
| 
       21 
23 
     | 
    
         
             
            require 'gitlab/experiment/version'
         
     | 
| 
       22 
24 
     | 
    
         
             
            require 'gitlab/experiment/engine' if defined?(Rails::Engine)
         
     | 
| 
         @@ -102,6 +104,8 @@ module Gitlab 
     | 
|
| 
       102 
104 
     | 
    
         | 
| 
       103 
105 
     | 
    
         
             
                def run(variant_name = nil)
         
     | 
| 
       104 
106 
     | 
    
         
             
                  @result ||= super(variant(variant_name).name)
         
     | 
| 
      
 107 
     | 
    
         
            +
                rescue Scientist::BehaviorMissing => e
         
     | 
| 
      
 108 
     | 
    
         
            +
                  raise Error, e
         
     | 
| 
       105 
109 
     | 
    
         
             
                end
         
     | 
| 
       106 
110 
     | 
    
         | 
| 
       107 
111 
     | 
    
         
             
                def publish(result)
         
     | 
| 
         @@ -116,6 +120,13 @@ module Gitlab 
     | 
|
| 
       116 
120 
     | 
    
         
             
                  instance_exec(action, event_args, &Configuration.tracking_behavior)
         
     | 
| 
       117 
121 
     | 
    
         
             
                end
         
     | 
| 
       118 
122 
     | 
    
         | 
| 
      
 123 
     | 
    
         
            +
                def process_redirect_url(url)
         
     | 
| 
      
 124 
     | 
    
         
            +
                  return unless Configuration.redirect_url_validator&.call(url)
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                  track('visited', url: url)
         
     | 
| 
      
 127 
     | 
    
         
            +
                  url # return the url, which allows for mutation
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
       119 
130 
     | 
    
         
             
                def enabled?
         
     | 
| 
       120 
131 
     | 
    
         
             
                  true
         
     | 
| 
       121 
132 
     | 
    
         
             
                end
         
     | 
| 
         @@ -139,11 +150,25 @@ module Gitlab 
     | 
|
| 
       139 
150 
     | 
    
         
             
                end
         
     | 
| 
       140 
151 
     | 
    
         | 
| 
       141 
152 
     | 
    
         
             
                def key_for(source, seed = name)
         
     | 
| 
       142 
     | 
    
         
            -
                   
     | 
| 
      
 153 
     | 
    
         
            +
                  # TODO: Added deprecation in release 0.6.0
         
     | 
| 
      
 154 
     | 
    
         
            +
                  if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
         
     | 
| 
      
 155 
     | 
    
         
            +
                    return instance_exec(source, seed, &block)
         
     | 
| 
      
 156 
     | 
    
         
            +
                  end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                  source = source.keys + source.values if source.is_a?(Hash)
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                  ingredients = Array(source).map { |v| identify(v) }
         
     | 
| 
      
 161 
     | 
    
         
            +
                  ingredients.unshift(seed).unshift(Configuration.context_key_secret)
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                  Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
         
     | 
| 
       143 
164 
     | 
    
         
             
                end
         
     | 
| 
       144 
165 
     | 
    
         | 
| 
       145 
166 
     | 
    
         
             
                protected
         
     | 
| 
       146 
167 
     | 
    
         | 
| 
      
 168 
     | 
    
         
            +
                def identify(object)
         
     | 
| 
      
 169 
     | 
    
         
            +
                  (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
         
     | 
| 
      
 170 
     | 
    
         
            +
                end
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
       147 
172 
     | 
    
         
             
                def segmentation_callback_chain
         
     | 
| 
       148 
173 
     | 
    
         
             
                  return :segmentation_check if @variant_name.nil? && enabled? && !excluded?
         
     | 
| 
       149 
174 
     | 
    
         | 
| 
         @@ -26,6 +26,12 @@ module Gitlab 
     | 
|
| 
       26 
26 
     | 
    
         | 
| 
       27 
27 
     | 
    
         
             
                      experiment_name(name).classify.safe_constantize || Configuration.base_class.constantize
         
     | 
| 
       28 
28 
     | 
    
         
             
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    def from_param(id)
         
     | 
| 
      
 31 
     | 
    
         
            +
                      %r{/?(?<name>.*):(?<key>.*)$} =~ id
         
     | 
| 
      
 32 
     | 
    
         
            +
                      name = CGI.unescape(name) if name
         
     | 
| 
      
 33 
     | 
    
         
            +
                      constantize(name).new(name).tap { |e| e.context.key(key) }
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
       29 
35 
     | 
    
         
             
                  end
         
     | 
| 
       30 
36 
     | 
    
         | 
| 
       31 
37 
     | 
    
         
             
                  def initialize(name = nil, variant_name = nil, **context)
         
     | 
| 
         @@ -45,9 +51,10 @@ module Gitlab 
     | 
|
| 
       45 
51 
     | 
    
         
             
                  end
         
     | 
| 
       46 
52 
     | 
    
         | 
| 
       47 
53 
     | 
    
         
             
                  def id
         
     | 
| 
       48 
     | 
    
         
            -
                    "#{name}:#{ 
     | 
| 
      
 54 
     | 
    
         
            +
                    "#{name}:#{context.key}"
         
     | 
| 
       49 
55 
     | 
    
         
             
                  end
         
     | 
| 
       50 
56 
     | 
    
         
             
                  alias_method :session_id, :id
         
     | 
| 
      
 57 
     | 
    
         
            +
                  alias_method :to_param, :id
         
     | 
| 
       51 
58 
     | 
    
         | 
| 
       52 
59 
     | 
    
         
             
                  def flipper_id
         
     | 
| 
       53 
60 
     | 
    
         
             
                    "Experiment;#{id}"
         
     | 
| 
         @@ -31,15 +31,27 @@ module Gitlab 
     | 
|
| 
       31 
31 
     | 
    
         
             
                  # experiments.
         
     | 
| 
       32 
32 
     | 
    
         
             
                  @default_rollout = Rollout::Base.new
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
      
 34 
     | 
    
         
            +
                  # Secret seed used in generating context keys.
         
     | 
| 
      
 35 
     | 
    
         
            +
                  @context_key_secret = nil
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  # Bit length used by SHA2 in generating context keys - (256, 384 or 512.)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @context_key_bit_length = 256
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  # The default base path that the middleware (or rails engine) will be
         
     | 
| 
      
 41 
     | 
    
         
            +
                  # mounted.
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @mount_at = nil
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  # The middleware won't redirect to urls that aren't considered valid.
         
     | 
| 
      
 45 
     | 
    
         
            +
                  # Expected to return a boolean value.
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @redirect_url_validator = ->(_redirect_url) { true }
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
       34 
48 
     | 
    
         
             
                  # Logic this project uses to determine inclusion in a given experiment.
         
     | 
| 
       35 
49 
     | 
    
         
             
                  # Expected to return a boolean value.
         
     | 
| 
       36 
     | 
    
         
            -
                  @inclusion_resolver =  
     | 
| 
       37 
     | 
    
         
            -
                    false
         
     | 
| 
       38 
     | 
    
         
            -
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @inclusion_resolver = ->(_requested_variant) { false }
         
     | 
| 
       39 
51 
     | 
    
         | 
| 
       40 
52 
     | 
    
         
             
                  # Tracking behavior can be implemented to link an event to an experiment.
         
     | 
| 
       41 
53 
     | 
    
         
             
                  @tracking_behavior = lambda do |event, args|
         
     | 
| 
       42 
     | 
    
         
            -
                    Configuration.logger.info 
     | 
| 
      
 54 
     | 
    
         
            +
                    Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature: signature)}")
         
     | 
| 
       43 
55 
     | 
    
         
             
                  end
         
     | 
| 
       44 
56 
     | 
    
         | 
| 
       45 
57 
     | 
    
         
             
                  # Called at the end of every experiment run, with the result.
         
     | 
| 
         @@ -47,24 +59,24 @@ module Gitlab 
     | 
|
| 
       47 
59 
     | 
    
         
             
                    track(:assignment)
         
     | 
| 
       48 
60 
     | 
    
         
             
                  end
         
     | 
| 
       49 
61 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
                  # Algorithm that consistently generates a hash key for a given source.
         
     | 
| 
       51 
     | 
    
         
            -
                  @context_hash_strategy = lambda do |source, seed|
         
     | 
| 
       52 
     | 
    
         
            -
                    source = source.keys + source.values if source.is_a?(Hash)
         
     | 
| 
       53 
     | 
    
         
            -
                    data = Array(source).map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
         
     | 
| 
       54 
     | 
    
         
            -
                    Digest::MD5.hexdigest(data.unshift(seed).join('|'))
         
     | 
| 
       55 
     | 
    
         
            -
                  end
         
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
62 
     | 
    
         
             
                  class << self
         
     | 
| 
      
 63 
     | 
    
         
            +
                    # TODO: Added deprecation in release 0.6.0
         
     | 
| 
      
 64 
     | 
    
         
            +
                    def context_hash_strategy=(block)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      ActiveSupport::Deprecation.warn('context_hash_strategy has been deprecated, instead configure' \
         
     | 
| 
      
 66 
     | 
    
         
            +
                        ' `context_key_secret` and `context_key_bit_length`.')
         
     | 
| 
      
 67 
     | 
    
         
            +
                      @__context_hash_strategy = block
         
     | 
| 
      
 68 
     | 
    
         
            +
                    end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
       58 
70 
     | 
    
         
             
                    # TODO: Added deprecation in release 0.5.0
         
     | 
| 
       59 
71 
     | 
    
         
             
                    def variant_resolver
         
     | 
| 
       60 
72 
     | 
    
         
             
                      ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
         
     | 
| 
       61 
     | 
    
         
            -
                        'block that returns a boolean.')
         
     | 
| 
      
 73 
     | 
    
         
            +
                        ' block that returns a boolean.')
         
     | 
| 
       62 
74 
     | 
    
         
             
                      @inclusion_resolver
         
     | 
| 
       63 
75 
     | 
    
         
             
                    end
         
     | 
| 
       64 
76 
     | 
    
         | 
| 
       65 
77 
     | 
    
         
             
                    def variant_resolver=(block)
         
     | 
| 
       66 
78 
     | 
    
         
             
                      ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
         
     | 
| 
       67 
     | 
    
         
            -
                        'block that returns a boolean.')
         
     | 
| 
      
 79 
     | 
    
         
            +
                        ' block that returns a boolean.')
         
     | 
| 
       68 
80 
     | 
    
         
             
                      @inclusion_resolver = block
         
     | 
| 
       69 
81 
     | 
    
         
             
                    end
         
     | 
| 
       70 
82 
     | 
    
         | 
| 
         @@ -74,11 +86,14 @@ module Gitlab 
     | 
|
| 
       74 
86 
     | 
    
         
             
                      :base_class,
         
     | 
| 
       75 
87 
     | 
    
         
             
                      :cache,
         
     | 
| 
       76 
88 
     | 
    
         
             
                      :cookie_domain,
         
     | 
| 
      
 89 
     | 
    
         
            +
                      :context_key_secret,
         
     | 
| 
      
 90 
     | 
    
         
            +
                      :context_key_bit_length,
         
     | 
| 
      
 91 
     | 
    
         
            +
                      :mount_at,
         
     | 
| 
       77 
92 
     | 
    
         
             
                      :default_rollout,
         
     | 
| 
      
 93 
     | 
    
         
            +
                      :redirect_url_validator,
         
     | 
| 
       78 
94 
     | 
    
         
             
                      :inclusion_resolver,
         
     | 
| 
       79 
95 
     | 
    
         
             
                      :tracking_behavior,
         
     | 
| 
       80 
     | 
    
         
            -
                      :publishing_behavior 
     | 
| 
       81 
     | 
    
         
            -
                      :context_hash_strategy
         
     | 
| 
      
 96 
     | 
    
         
            +
                      :publishing_behavior
         
     | 
| 
       82 
97 
     | 
    
         
             
                    )
         
     | 
| 
       83 
98 
     | 
    
         
             
                  end
         
     | 
| 
       84 
99 
     | 
    
         
             
                end
         
     | 
| 
         @@ -31,6 +31,12 @@ module Gitlab 
     | 
|
| 
       31 
31 
     | 
    
         
             
                    @value.merge!(process_migrations(value))
         
     | 
| 
       32 
32 
     | 
    
         
             
                  end
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
      
 34 
     | 
    
         
            +
                  def key(key = nil)
         
     | 
| 
      
 35 
     | 
    
         
            +
                    return @key || @experiment.key_for(value) if key.nil?
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                    @key = key
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
       34 
40 
     | 
    
         
             
                  def trackable?
         
     | 
| 
       35 
41 
     | 
    
         
             
                    !(@request && @request.headers['DNT'].to_s.match?(DNT_REGEXP))
         
     | 
| 
       36 
42 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -41,7 +47,7 @@ module Gitlab 
     | 
|
| 
       41 
47 
     | 
    
         
             
                  end
         
     | 
| 
       42 
48 
     | 
    
         | 
| 
       43 
49 
     | 
    
         
             
                  def signature
         
     | 
| 
       44 
     | 
    
         
            -
                    @signature ||= { key:  
     | 
| 
      
 50 
     | 
    
         
            +
                    @signature ||= { key: key, migration_keys: migration_keys }.compact
         
     | 
| 
       45 
51 
     | 
    
         
             
                  end
         
     | 
| 
       46 
52 
     | 
    
         | 
| 
       47 
53 
     | 
    
         
             
                  def method_missing(method_name, *)
         
     | 
| 
         @@ -55,16 +61,18 @@ module Gitlab 
     | 
|
| 
       55 
61 
     | 
    
         
             
                  private
         
     | 
| 
       56 
62 
     | 
    
         | 
| 
       57 
63 
     | 
    
         
             
                  def process_migrations(value)
         
     | 
| 
       58 
     | 
    
         
            -
                     
     | 
| 
       59 
     | 
    
         
            -
                     
     | 
| 
      
 64 
     | 
    
         
            +
                    add_unmerged_migration(value.delete(:migrated_from))
         
     | 
| 
      
 65 
     | 
    
         
            +
                    add_merged_migration(value.delete(:migrated_with))
         
     | 
| 
       60 
66 
     | 
    
         | 
| 
       61 
67 
     | 
    
         
             
                    migrate_cookie(value, "#{@experiment.name}_id")
         
     | 
| 
       62 
68 
     | 
    
         
             
                  end
         
     | 
| 
       63 
69 
     | 
    
         | 
| 
       64 
     | 
    
         
            -
                  def  
     | 
| 
       65 
     | 
    
         
            -
                     
     | 
| 
      
 70 
     | 
    
         
            +
                  def add_unmerged_migration(value = {})
         
     | 
| 
      
 71 
     | 
    
         
            +
                    @migrations[:unmerged] << value if value.is_a?(Hash)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
       66 
73 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
             
     | 
| 
      
 74 
     | 
    
         
            +
                  def add_merged_migration(value = {})
         
     | 
| 
      
 75 
     | 
    
         
            +
                    @migrations[:merged] << value if value.is_a?(Hash)
         
     | 
| 
       68 
76 
     | 
    
         
             
                  end
         
     | 
| 
       69 
77 
     | 
    
         | 
| 
       70 
78 
     | 
    
         
             
                  def migration_keys
         
     | 
| 
         @@ -3,6 +3,10 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            module Gitlab
         
     | 
| 
       4 
4 
     | 
    
         
             
              class Experiment
         
     | 
| 
       5 
5 
     | 
    
         
             
                module Dsl
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def self.include_in(klass, with_helper: false)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    klass.include(self).tap { |base| base.helper_method(:experiment) if with_helper }
         
     | 
| 
      
 8 
     | 
    
         
            +
                  end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
       6 
10 
     | 
    
         
             
                  def experiment(name, variant_name = nil, **context, &block)
         
     | 
| 
       7 
11 
     | 
    
         
             
                    raise ArgumentError, 'name is required' if name.nil?
         
     | 
| 
       8 
12 
     | 
    
         | 
| 
         @@ -1,21 +1,46 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            require 'active_model'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
       3 
5 
     | 
    
         
             
            module Gitlab
         
     | 
| 
       4 
6 
     | 
    
         
             
              class Experiment
         
     | 
| 
      
 7 
     | 
    
         
            +
                include ActiveModel::Model
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                # used for generating routes
         
     | 
| 
      
 10 
     | 
    
         
            +
                def self.model_name
         
     | 
| 
      
 11 
     | 
    
         
            +
                  ActiveModel::Name.new(self, Gitlab)
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
       5 
14 
     | 
    
         
             
                class Engine < ::Rails::Engine
         
     | 
| 
       6 
     | 
    
         
            -
                   
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
                  isolate_namespace Experiment
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  initializer('gitlab_experiment.include_dsl') { include_dsl }
         
     | 
| 
      
 18 
     | 
    
         
            +
                  initializer('gitlab_experiment.mount_engine') { |app| mount_engine(app, Configuration.mount_at) }
         
     | 
| 
       11 
19 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
      
 20 
     | 
    
         
            +
                  private
         
     | 
| 
       13 
21 
     | 
    
         | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
                     
     | 
| 
      
 22 
     | 
    
         
            +
                  def include_dsl
         
     | 
| 
      
 23 
     | 
    
         
            +
                    Dsl.include_in(ActionController::Base, with_helper: true) if defined?(ActionController)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    Dsl.include_in(ActionMailer::Base, with_helper: true) if defined?(ActionMailer)
         
     | 
| 
       16 
25 
     | 
    
         
             
                  end
         
     | 
| 
       17 
26 
     | 
    
         | 
| 
       18 
     | 
    
         
            -
                   
     | 
| 
      
 27 
     | 
    
         
            +
                  def mount_engine(app, mount_at)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    return if mount_at.blank?
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    engine = routes do
         
     | 
| 
      
 31 
     | 
    
         
            +
                      default_url_options app.routes.default_url_options.clone.without(:script_name)
         
     | 
| 
      
 32 
     | 
    
         
            +
                      resources :experiments, path: '/', only: :show
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    app.config.middleware.use(Middleware, mount_at)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    app.routes.append do
         
     | 
| 
      
 37 
     | 
    
         
            +
                      mount Engine, at: mount_at, as: :experiment_engine
         
     | 
| 
      
 38 
     | 
    
         
            +
                      direct(:experiment_redirect) do |ex, options|
         
     | 
| 
      
 39 
     | 
    
         
            +
                        url = options[:url]
         
     | 
| 
      
 40 
     | 
    
         
            +
                        "#{engine.url_helpers.experiment_url(ex)}?#{url}"
         
     | 
| 
      
 41 
     | 
    
         
            +
                      end
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
       19 
44 
     | 
    
         
             
                end
         
     | 
| 
       20 
45 
     | 
    
         
             
              end
         
     | 
| 
       21 
46 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,27 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Gitlab
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Experiment
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Middleware
         
     | 
| 
      
 6 
     | 
    
         
            +
                  def self.redirect(id, url)
         
     | 
| 
      
 7 
     | 
    
         
            +
                    raise Error, 'no url to redirect to' if url.blank?
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    experiment = Gitlab::Experiment.from_param(id)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    [303, { 'Location' => experiment.process_redirect_url(url) || raise(Error, 'not redirecting') }, []]
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def initialize(app, base_path)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @app = app
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @matcher = %r{^#{base_path}/(?<id>.+)}
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def call(env)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    return @app.call(env) if env['REQUEST_METHOD'] != 'GET' || (match = @matcher.match(env['PATH_INFO'])).nil?
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    Middleware.redirect(match[:id], env['QUERY_STRING'])
         
     | 
| 
      
 22 
     | 
    
         
            +
                  rescue Error
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @app.call(env)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -3,6 +3,7 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            module Gitlab
         
     | 
| 
       4 
4 
     | 
    
         
             
              class Experiment
         
     | 
| 
       5 
5 
     | 
    
         
             
                module Rollout
         
     | 
| 
      
 6 
     | 
    
         
            +
                  autoload :Percent, 'gitlab/experiment/rollout/percent.rb'
         
     | 
| 
       6 
7 
     | 
    
         
             
                  autoload :Random, 'gitlab/experiment/rollout/random.rb'
         
     | 
| 
       7 
8 
     | 
    
         
             
                  autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
         
     | 
| 
       8 
9 
     | 
    
         | 
| 
         @@ -15,17 +16,23 @@ module Gitlab 
     | 
|
| 
       15 
16 
     | 
    
         
             
                  class Base
         
     | 
| 
       16 
17 
     | 
    
         
             
                    attr_reader :experiment
         
     | 
| 
       17 
18 
     | 
    
         | 
| 
       18 
     | 
    
         
            -
                    delegate :variant_names, :cache, to: :experiment
         
     | 
| 
      
 19 
     | 
    
         
            +
                    delegate :variant_names, :cache, :id, to: :experiment
         
     | 
| 
       19 
20 
     | 
    
         | 
| 
       20 
21 
     | 
    
         
             
                    def initialize(options = {})
         
     | 
| 
       21 
22 
     | 
    
         
             
                      @options = options
         
     | 
| 
      
 23 
     | 
    
         
            +
                      # validate! # we want to validate here, but we can't yet
         
     | 
| 
       22 
24 
     | 
    
         
             
                    end
         
     | 
| 
       23 
25 
     | 
    
         | 
| 
       24 
26 
     | 
    
         
             
                    def rollout_for(experiment)
         
     | 
| 
       25 
27 
     | 
    
         
             
                      @experiment = experiment
         
     | 
| 
      
 28 
     | 
    
         
            +
                      validate! # until we have variant registration we can only validate here
         
     | 
| 
       26 
29 
     | 
    
         
             
                      execute
         
     | 
| 
       27 
30 
     | 
    
         
             
                    end
         
     | 
| 
       28 
31 
     | 
    
         | 
| 
      
 32 
     | 
    
         
            +
                    def validate!
         
     | 
| 
      
 33 
     | 
    
         
            +
                      # base is always valid
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
       29 
36 
     | 
    
         
             
                    def execute
         
     | 
| 
       30 
37 
     | 
    
         
             
                      variant_names.first
         
     | 
| 
       31 
38 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -0,0 +1,47 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'zlib'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Gitlab
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Experiment
         
     | 
| 
      
 7 
     | 
    
         
            +
                module Rollout
         
     | 
| 
      
 8 
     | 
    
         
            +
                  class Percent < Base
         
     | 
| 
      
 9 
     | 
    
         
            +
                    def execute
         
     | 
| 
      
 10 
     | 
    
         
            +
                      crc = normalized_id
         
     | 
| 
      
 11 
     | 
    
         
            +
                      total = 0
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                      case distribution_rules
         
     | 
| 
      
 14 
     | 
    
         
            +
                      # run through the rules until finding an acceptable one
         
     | 
| 
      
 15 
     | 
    
         
            +
                      when Array then variant_names[distribution_rules.find_index { |percent| crc % 100 <= total += percent }]
         
     | 
| 
      
 16 
     | 
    
         
            +
                      # run through the variant names until finding an acceptable one
         
     | 
| 
      
 17 
     | 
    
         
            +
                      when Hash then distribution_rules.find { |_, percent| crc % 100 <= total += percent }.first
         
     | 
| 
      
 18 
     | 
    
         
            +
                      # when there are no rules, assume even distribution
         
     | 
| 
      
 19 
     | 
    
         
            +
                      else variant_names[crc % variant_names.length]
         
     | 
| 
      
 20 
     | 
    
         
            +
                      end
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    def validate!
         
     | 
| 
      
 24 
     | 
    
         
            +
                      case distribution_rules
         
     | 
| 
      
 25 
     | 
    
         
            +
                      when nil then nil
         
     | 
| 
      
 26 
     | 
    
         
            +
                      when Array, Hash
         
     | 
| 
      
 27 
     | 
    
         
            +
                        if distribution_rules.length != variant_names.length
         
     | 
| 
      
 28 
     | 
    
         
            +
                          raise InvalidRolloutRules, "the distribution rules don't match the number of variants defined"
         
     | 
| 
      
 29 
     | 
    
         
            +
                        end
         
     | 
| 
      
 30 
     | 
    
         
            +
                      else
         
     | 
| 
      
 31 
     | 
    
         
            +
                        raise InvalidRolloutRules, 'unknown distribution options type'
         
     | 
| 
      
 32 
     | 
    
         
            +
                      end
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    private
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                    def normalized_id
         
     | 
| 
      
 38 
     | 
    
         
            +
                      Zlib.crc32(id, nil)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                    def distribution_rules
         
     | 
| 
      
 42 
     | 
    
         
            +
                      @options[:distribution]
         
     | 
| 
      
 43 
     | 
    
         
            +
                    end
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
              end
         
     | 
| 
      
 47 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -82,7 +82,7 @@ module Gitlab 
     | 
|
| 
       82 
82 
     | 
    
         
             
                  extend RSpec::Matchers::DSL
         
     | 
| 
       83 
83 
     | 
    
         | 
| 
       84 
84 
     | 
    
         
             
                  def require_experiment(experiment, matcher_name, classes: false)
         
     | 
| 
       85 
     | 
    
         
            -
                    klass = experiment. 
     | 
| 
      
 85 
     | 
    
         
            +
                    klass = experiment.instance_of?(Class) ? experiment : experiment.class
         
     | 
| 
       86 
86 
     | 
    
         
             
                    unless klass <= Gitlab::Experiment
         
     | 
| 
       87 
87 
     | 
    
         
             
                      raise(
         
     | 
| 
       88 
88 
     | 
    
         
             
                        ArgumentError,
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: gitlab-experiment
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.6.4
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - GitLab
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire: 
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date:  
     | 
| 
      
 11 
     | 
    
         
            +
            date: 
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: activesupport
         
     | 
| 
         @@ -65,35 +65,52 @@ executables: [] 
     | 
|
| 
       65 
65 
     | 
    
         
             
            extensions: []
         
     | 
| 
       66 
66 
     | 
    
         
             
            extra_rdoc_files: []
         
     | 
| 
       67 
67 
     | 
    
         
             
            files:
         
     | 
| 
       68 
     | 
    
         
            -
            -  
     | 
| 
       69 
     | 
    
         
            -
            -  
     | 
| 
       70 
     | 
    
         
            -
            - lib/generators/gitlab/experiment/ 
     | 
| 
       71 
     | 
    
         
            -
            - lib/generators/gitlab/experiment/experiment_generator.rb
         
     | 
| 
      
 68 
     | 
    
         
            +
            - lib/generators/gitlab
         
     | 
| 
      
 69 
     | 
    
         
            +
            - lib/generators/gitlab/experiment
         
     | 
| 
      
 70 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/install
         
     | 
| 
       72 
71 
     | 
    
         
             
            - lib/generators/gitlab/experiment/install/install_generator.rb
         
     | 
| 
       73 
     | 
    
         
            -
            - lib/generators/gitlab/experiment/install/templates 
     | 
| 
      
 72 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/install/templates
         
     | 
| 
       74 
73 
     | 
    
         
             
            - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
         
     | 
| 
       75 
74 
     | 
    
         
             
            - lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
         
     | 
| 
      
 75 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
         
     | 
| 
      
 76 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/USAGE
         
     | 
| 
      
 77 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/experiment_generator.rb
         
     | 
| 
      
 78 
     | 
    
         
            +
            - lib/generators/gitlab/experiment/templates
         
     | 
| 
       76 
79 
     | 
    
         
             
            - lib/generators/gitlab/experiment/templates/experiment.rb.tt
         
     | 
| 
       77 
     | 
    
         
            -
            - lib/generators/ 
     | 
| 
       78 
     | 
    
         
            -
            - lib/generators/ 
     | 
| 
      
 80 
     | 
    
         
            +
            - lib/generators/test_unit
         
     | 
| 
      
 81 
     | 
    
         
            +
            - lib/generators/test_unit/experiment
         
     | 
| 
       79 
82 
     | 
    
         
             
            - lib/generators/test_unit/experiment/experiment_generator.rb
         
     | 
| 
      
 83 
     | 
    
         
            +
            - lib/generators/test_unit/experiment/templates
         
     | 
| 
       80 
84 
     | 
    
         
             
            - lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
         
     | 
| 
      
 85 
     | 
    
         
            +
            - lib/generators/rspec
         
     | 
| 
      
 86 
     | 
    
         
            +
            - lib/generators/rspec/experiment
         
     | 
| 
      
 87 
     | 
    
         
            +
            - lib/generators/rspec/experiment/experiment_generator.rb
         
     | 
| 
      
 88 
     | 
    
         
            +
            - lib/generators/rspec/experiment/templates
         
     | 
| 
      
 89 
     | 
    
         
            +
            - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
         
     | 
| 
       81 
90 
     | 
    
         
             
            - lib/gitlab/experiment.rb
         
     | 
| 
       82 
     | 
    
         
            -
            - lib/gitlab/experiment 
     | 
| 
       83 
     | 
    
         
            -
            - lib/gitlab/experiment/ 
     | 
| 
      
 91 
     | 
    
         
            +
            - lib/gitlab/experiment
         
     | 
| 
      
 92 
     | 
    
         
            +
            - lib/gitlab/experiment/variant.rb
         
     | 
| 
      
 93 
     | 
    
         
            +
            - lib/gitlab/experiment/middleware.rb
         
     | 
| 
      
 94 
     | 
    
         
            +
            - lib/gitlab/experiment/cache
         
     | 
| 
       84 
95 
     | 
    
         
             
            - lib/gitlab/experiment/cache/redis_hash_store.rb
         
     | 
| 
      
 96 
     | 
    
         
            +
            - lib/gitlab/experiment/errors.rb
         
     | 
| 
       85 
97 
     | 
    
         
             
            - lib/gitlab/experiment/callbacks.rb
         
     | 
| 
       86 
     | 
    
         
            -
            - lib/gitlab/experiment/ 
     | 
| 
      
 98 
     | 
    
         
            +
            - lib/gitlab/experiment/rollout.rb
         
     | 
| 
      
 99 
     | 
    
         
            +
            - lib/gitlab/experiment/base_interface.rb
         
     | 
| 
       87 
100 
     | 
    
         
             
            - lib/gitlab/experiment/context.rb
         
     | 
| 
       88 
     | 
    
         
            -
            - lib/gitlab/experiment/cookies.rb
         
     | 
| 
       89 
     | 
    
         
            -
            - lib/gitlab/experiment/dsl.rb
         
     | 
| 
       90 
101 
     | 
    
         
             
            - lib/gitlab/experiment/engine.rb
         
     | 
| 
       91 
     | 
    
         
            -
            - lib/gitlab/experiment/ 
     | 
| 
      
 102 
     | 
    
         
            +
            - lib/gitlab/experiment/rspec.rb
         
     | 
| 
      
 103 
     | 
    
         
            +
            - lib/gitlab/experiment/rollout
         
     | 
| 
       92 
104 
     | 
    
         
             
            - lib/gitlab/experiment/rollout/random.rb
         
     | 
| 
       93 
105 
     | 
    
         
             
            - lib/gitlab/experiment/rollout/round_robin.rb
         
     | 
| 
       94 
     | 
    
         
            -
            - lib/gitlab/experiment/ 
     | 
| 
       95 
     | 
    
         
            -
            - lib/gitlab/experiment/ 
     | 
| 
      
 106 
     | 
    
         
            +
            - lib/gitlab/experiment/rollout/percent.rb
         
     | 
| 
      
 107 
     | 
    
         
            +
            - lib/gitlab/experiment/cache.rb
         
     | 
| 
       96 
108 
     | 
    
         
             
            - lib/gitlab/experiment/version.rb
         
     | 
| 
      
 109 
     | 
    
         
            +
            - lib/gitlab/experiment/cookies.rb
         
     | 
| 
      
 110 
     | 
    
         
            +
            - lib/gitlab/experiment/configuration.rb
         
     | 
| 
      
 111 
     | 
    
         
            +
            - lib/gitlab/experiment/dsl.rb
         
     | 
| 
      
 112 
     | 
    
         
            +
            - LICENSE.txt
         
     | 
| 
      
 113 
     | 
    
         
            +
            - README.md
         
     | 
| 
       97 
114 
     | 
    
         
             
            homepage: https://gitlab.com/gitlab-org/gitlab-experiment
         
     | 
| 
       98 
115 
     | 
    
         
             
            licenses:
         
     | 
| 
       99 
116 
     | 
    
         
             
            - MIT
         
     | 
| 
         @@ -113,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       113 
130 
     | 
    
         
             
                - !ruby/object:Gem::Version
         
     | 
| 
       114 
131 
     | 
    
         
             
                  version: '0'
         
     | 
| 
       115 
132 
     | 
    
         
             
            requirements: []
         
     | 
| 
       116 
     | 
    
         
            -
            rubygems_version: 3. 
     | 
| 
      
 133 
     | 
    
         
            +
            rubygems_version: 3.1.4
         
     | 
| 
       117 
134 
     | 
    
         
             
            signing_key: 
         
     | 
| 
       118 
135 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       119 
136 
     | 
    
         
             
            summary: GitLab experiment library built on top of scientist.
         
     |