File: configuration.rb

package info (click to toggle)
ruby-gitlab-experiment 0.9.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 260 kB
  • sloc: ruby: 1,202; makefile: 7
file content (222 lines) | stat: -rw-r--r-- 8,224 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# frozen_string_literal: true

require 'singleton'
require 'logger'
require 'digest'

module Gitlab
  class Experiment
    class Configuration
      include Singleton

      # Prefix all experiment names with a given string value.
      # Use `nil` for no prefix.
      @name_prefix = nil

      # The logger can be used to log various details of the experiments.
      @logger = Logger.new($stdout)

      # The base class that should be instantiated for basic experiments.
      # It should be a string, so we can constantize it later.
      @base_class = 'Gitlab::Experiment'

      # Require experiments to be defined in a class, with variants registered.
      # This will disallow any anonymous experiments that are run inline
      # without previously defining a class.
      @strict_registration = false

      # The caching layer is expected to match the Rails.cache interface.
      # If no cache is provided some rollout strategies may behave differently.
      # Use `nil` for no caching.
      @cache = nil

      # The domain to use on cookies.
      #
      # When not set, it uses the current host. If you want to provide specific
      # hosts, you use `:all`, or provide an array.
      #
      # Examples:
      #   nil, :all, or ['www.gitlab.com', '.gitlab.com']
      @cookie_domain = :all

      # The cookie name for an experiment.
      @cookie_name = lambda do |experiment|
        "#{experiment.name}_id"
      end

      # The default rollout strategy.
      #
      # The recommended default rollout strategy when not using caching would
      # be `Gitlab::Experiment::Rollout::Percent` as that will consistently
      # assign the same variant with or without caching.
      #
      # Gitlab::Experiment::Rollout::Base can be inherited to implement your
      # own rollout strategies.
      #
      # Each experiment can specify its own rollout strategy:
      #
      # class ExampleExperiment < ApplicationExperiment
      #   default_rollout :random # :percent, :round_robin, or MyCustomRollout
      # end
      #
      # Included rollout strategies:
      #   :percent, (recommended), :round_robin, or :random
      @default_rollout = Rollout.resolve(:percent)

      # Secret seed used in generating context keys.
      #
      # You'll typically want to use an environment variable or secret value
      # for this.
      #
      # Consider not using one that's shared with other systems, like Rails'
      # SECRET_KEY_BASE for instance. Generate a new secret and utilize that
      # instead.
      @context_key_secret = nil

      # Bit length used by SHA2 in generating context keys.
      #
      # Using a higher bit length would require more computation time.
      #
      # Valid bit lengths:
      #   256, 384, or 512
      @context_key_bit_length = 256

      # The default base path that the middleware (or rails engine) will be
      # mounted. The middleware enables an instrumentation url, that's similar
      # to links that can be instrumented in email campaigns.
      #
      # Use `nil` if you don't want to mount the middleware.
      #
      # Examples:
      #   '/-/experiment', '/redirect', nil
      @mount_at = nil

      # When using the middleware, links can be instrumented and redirected
      # elsewhere. This can be exploited to make a harmful url look innocuous
      # or that it's a valid url on your domain. To avoid this, you can provide
      # your own logic for what urls will be considered valid and redirected
      # to.
      #
      # Expected to return a boolean value.
      @redirect_url_validator = lambda do |_redirect_url|
        true
      end

      # Tracking behavior can be implemented to link an event to an experiment.
      #
      # This block is executed within the scope of the experiment and so can
      # access experiment methods, like `name`, `context`, and `signature`.
      @tracking_behavior = lambda do |event, args|
        # An example of using a generic logger to track events:
        Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature: signature)}")

        # Using something like snowplow to track events (in gitlab):
        #
        # Gitlab::Tracking.event(name, event, **args.merge(
        #   context: (args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(
        #     'iglu:com.gitlab/gitlab_experiment/jsonschema/0-2-0', signature
        #   )
        # ))
      end

      # Logic designed to respond when a given experiment is nested within
      # another experiment. This can be useful to identify overlaps and when a
      # code path leads to an experiment being nested within another.
      #
      # Reporting complexity can arise when one experiment changes rollout, and
      # a downstream experiment is impacted by that.
      #
      # The base_class or a custom experiment can provide a `nest_experiment`
      # method that implements its own logic that may allow certain experiments
      # to be nested within it.
      #
      # This block is executed within the scope of the experiment and so can
      # access experiment methods, like `name`, `context`, and `signature`.
      #
      # The default exception will include the where the experiment calls were
      # initiated on, so for instance:
      #
      # Gitlab::Experiment::NestingError: unable to nest level2 within level1:
      #   level1 initiated by file_name.rb:2
      #   level2 initiated by file_name.rb:3
      @nested_behavior = lambda do |nested_experiment|
        raise NestingError.new(experiment: self, nested_experiment: nested_experiment)
      end

      # Called at the end of every experiment run, with the result.
      #
      # You may want to track that you've assigned a variant to a given
      # context, or push the experiment into the client or publish results
      # elsewhere like into redis.
      #
      # This block is executed within the scope of the experiment and so can
      # access experiment methods, like `name`, `context`, and `signature`.
      @publishing_behavior = lambda do |_result|
        # Track the event using our own configured tracking logic.
        track(:assignment)

        # Log using our logging system, so the result (which can be large) can
        # be reviewed later if we want to.
        #
        # Lograge::Event.log(experiment: name, result: result, signature: signature)

        # Experiments that have been run during the request lifecycle can be
        # pushed to the client layer by injecting the published experiments
        # into javascript in a layout or view using something like:
        #
        # = javascript_tag(nonce: content_security_policy_nonce) do
        #   window.experiments = #{raw Gitlab::Experiment.published_experiments.to_json};
      end

      class << self
        attr_accessor(
          :name_prefix,
          :logger,
          :base_class,
          :strict_registration,
          :cache,
          :cookie_domain,
          :cookie_name,
          :context_key_secret,
          :context_key_bit_length,
          :mount_at,
          :default_rollout,
          :redirect_url_validator,
          :tracking_behavior,
          :nested_behavior,
          :publishing_behavior
        )

        # Attribute method overrides.

        def default_rollout=(args) # rubocop:disable Lint/DuplicateMethods
          @default_rollout = Rollout.resolve(*args)
        end

        # Internal warning helpers.

        def deprecated(*args, version:, stack: 0)
          deprecator = deprecator(version)
          args << args.pop.to_s.gsub('{{release}}', "#{deprecator.gem_name} #{deprecator.deprecation_horizon}")
          args << caller_locations(4 + stack)

          if args.length == 2
            deprecator.warn(*args)
          else
            args[0] = "`#{args[0]}`"
            deprecator.deprecation_warning(*args)
          end
        end

        private

        def deprecator(version = VERSION)
          version = Gem::Version.new(version).bump.to_s

          @__dep_versions ||= {}
          @__dep_versions[version] ||= ActiveSupport::Deprecation.new(version, 'Gitlab::Experiment')
        end
      end
    end
  end
end