eeny-meeny 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e28ef5a3f25b3acf3e554f578623aab014bc8033
4
- data.tar.gz: 017bf53a5cca472993e25c1d09cc514e3a0983b9
3
+ metadata.gz: 34311644231b710c6ce1e10c5fb6f4410bd4ed5d
4
+ data.tar.gz: fa951cdb99821c5cd8d0c032d0048875b210193f
5
5
  SHA512:
6
- metadata.gz: e188c4dd1ceeee62fdd3efc5ab445808d49b34cb75579dbe591c381c6fab73c2206261473ad27d7dd8df3e72ca4d5d86508ef5d97ad8aba2c2661e2dee926b58
7
- data.tar.gz: e7f33467b3c326a158405a528e4ca787afc05061fca5e0fa4581a083f00f7523bc9ebbb6bb1e0d36e1274e41d09e712fef0685bc527c4630de6b89bb1d970437
6
+ metadata.gz: 46a37bb2552e814042c625c3e864073ac4133f295eec99e8a646b4331182060299981b3e8248e7bda8f3c5c2c629ae276a49f0ca9c4290700c22fc840ee0f508
7
+ data.tar.gz: 03653ee7e0e9eee45bfe2f0efe87e92df72bf7a74d6546dfae2947330eba02e8dd218bb0f070b55967081b7f00be3dd501596b92af8328b65d3c9d150cb5b6f3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### 2.0.0 (2016-09-11)
2
+
3
+ Features:
4
+
5
+ - Added helper and route constraint for smoke tests.
6
+ - Added rake tasks for creating eeny-meeny cookies from the commandline.
7
+
8
+ Bugfixes:
9
+
10
+ - Fixed experiment start_at and end_at logic.
11
+
12
+ Breaking Changes:
13
+
14
+ - Changed the way the gem is configured.
15
+ - Replaced EenyMeeny::RouteConstraint with EenyMeeny::ExperimentConstraint and EenyMeeny::SmokeTestConstraint.
16
+
17
+ Other Changes:
18
+
19
+ - Changed default cookie expires header from '1.year.from_now' to '1.month.from_now'.
20
+ - Improve docuemtation.
21
+
1
22
  ### 1.0.0 (2016-07-03)
2
23
 
3
24
  Features:
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  eeny-meeny
2
2
  ==========================
3
+ [![Gem Version](https://badge.fury.io/rb/eeny-meeny.svg)](https://badge.fury.io/rb/eeny-meeny)
3
4
  [![Code Climate](https://codeclimate.com/github/corthmann/eeny-meeny/badges/gpa.svg)](https://codeclimate.com/github/corthmann/eeny-meeny)
4
5
  [![Test Coverage](https://codeclimate.com/github/corthmann/eeny-meeny/badges/coverage.svg)](https://codeclimate.com/github/corthmann/eeny-meeny/coverage)
5
6
 
@@ -20,16 +21,17 @@ Configuration
20
21
 
21
22
  The following configurations are available:
22
23
 
23
- * `config.eeny_meeny.cookies.path` Defaults to `'/'`. Sets the `path` cookie attribute. If this configuartion is set to `nil` it means that each page will get its own cookie.
24
- * `config.eeny_meeny.cookies.same_site` Defaults to `:strict`. Accepts: `:strict`, `:lax` and `nil`. Sets the `SameSite` cookie attribute. Selecting `nil` will disable the header on the cookie.
25
- * `config.eeny_meeny.secure` Boolean value. Defaults to `true` and determines if experiment cookies should be encrypted or not.
26
- * `config.eeny_meeny.secret` sets the secret used for encrypting experiment cookies.
27
- * `config.eeny_meeny.experiments` list of experiment-data. It is easiest to load this from a `.yml` file with the following structure:
24
+ * `cookies` Defaults to `{ http_only: true, path: '/', same_site: :strict }`. Sets the eeny-meeny cookie attributes. The valid attributes are listed in the section below.
25
+ * `secure` Defaults to `true`. Determines if eeny-meeny cookies should be encrypted or not.
26
+ * `secret` Sets the secret used for encrypting experiment cookies.
27
+ * `experiments` Defaults to `{}`. It is easiest to load this from a `.yml` file with `YAML.load_file(File.join('config','experiments.yml'))`. The YAML file should have a structure matching the following example:
28
28
 
29
29
  ```
30
30
  :experiment_1:
31
31
  :name: Awesome Experiment
32
32
  :version: 1
33
+ :start_at: '2026-08-11T11:55:40Z'
34
+ :end_at: '2026-08-11T11:55:40Z'
33
35
  :variations:
34
36
  :a:
35
37
  :name: Variation A
@@ -43,11 +45,132 @@ The following configurations are available:
43
45
  :message: B is an all-star!
44
46
  ```
45
47
 
46
- Usage
48
+ Valid cookie attributes:
49
+
50
+ * `domain` Sets the domain scope for the eeny-meeny cookies.
51
+ * `expires` Sets the date/time where the cookie gets deleted from the browser. If `end_at` have been specified for the experiment, then the `end_at` time will be used. Otherwise this value will default to `1.month.from_now`.
52
+ * `httponly` Directs browsers not to expose cookies through channels other than HTTP (and HTTPS) requests.
53
+ * `max_age` Can be used to set the cookie's expiration as an interval of seconds in the future, relative to the time the browser received the cookie.
54
+ * `path` Sets the `path` cookie attribute. If this configuration is set to `nil` it means that each page will get its own cookie.
55
+ * `same_site` Accepts: `:strict`, `:lax` and `nil`. Sets the `SameSite` cookie attribute. Selecting `nil` will disable the header on the cookie.
56
+ * `secure` Is meant to keep cookie communication limited to encrypted transmission, directing browsers to use cookies only via secure/encrypted connections.
57
+
58
+ You can find more information about cookie attributes at: https://en.wikipedia.org/wiki/HTTP_cookie#Cookie_attributes
59
+
60
+ Example configuration in Rails environment file:
61
+ ```
62
+ # load experiments and set secret. Use default cookies configuration.
63
+ config.eeny_meeny = {
64
+ experiments: YAML.load_file(File.join('config','experiments.yml')),
65
+ secret: 'my secret'
66
+ }
67
+ # disable encryption, httponly and set same_site to :lax.
68
+ config.eeny_meeny = {
69
+ cookies: { httponly: false, path: '/', same_site: :lax },
70
+ experiments: YAML.load_file(File.join('config','experiments.yml')),
71
+ secure: false
72
+ }
73
+ ```
74
+ Example configuration from initializer:
75
+ ```
76
+ # load experiments and set secret. Use default cookies configuration.
77
+ EenyMeeny.configure do |config|
78
+ config.experiments = YAML.load_file(File.join('config','experiments.yml'))
79
+ config.secret = 'my secret'
80
+ end
81
+ ```
82
+
83
+ Helpers
47
84
  -------------
48
85
  `eeny-meeny` adds the following helpers to your controllers and views:
49
86
 
50
- * `participates_in?(experiement_id, variation_id: nil)` Returns the chosen variation for the current user if he participates in the experiment.
87
+ * `participates_in?(experiement_id, variation_id: nil)` Returns the chosen variation for the current user if he participates in the experiment. Otherwise it returns `nil`.
88
+ * `smoke_test?(smoke_test_id, version: 1)` If the current user has a valid cookie for the smoke test that cookie value is returned. Otherwise it returns `nil`.
89
+
90
+ Route Constraints
91
+ -------------
92
+ `eeny-meeny` allows you to use the following route constraints:
93
+
94
+ * `EenyMeeny::ExperimentConstraint` allows you to route participants of an experiment and/or experiment variation to a different route than non-participants.
95
+ * `EenyMeeny::SmokeTestConstraint` allows you to route traffic from users with a smoke test cookie to a different route than non-participants.
96
+
97
+ In order to use the route constraints you need to require them in `routes.rb` (e.g. `require 'eeny-meeny/routing/experiment_constraint'` and `require 'eeny-meeny/routing/smoke_test_constraint'`)
98
+
99
+ Rake tasks
100
+ -------------
101
+ `eeny-meeny` adds the following rake tasks to your project.
102
+
103
+ * `eeny_meeny:cookies:experiment[experiment_id]`. Creates and outputs a valid cookie for the given experiment id.
104
+ * `eeny_meeny:cookies:experiment_variation[experiment_id,variation_id]` creates and outputs a valid cookie for the given variation of the experiment with the given experiment_id.
105
+ * `eeny_meeny:cookies:smoke_test[smoke_test_id,version]` Creates and outputs a valid smoke test cookie for a smoke test with the given id and version. `version` will default to `1` if not given.
106
+
107
+ You can execute the rake tasks like this:
108
+
109
+ * `rake eeny_meeny:cookies:experimet[experiment_id]`
110
+ * `rake eeny_meeny:cookies:experimet_variation[experiment_id, a]`
111
+ * `rake eeny_meeny:cookies:smoke_test[shadow]`
112
+ * `rake eeny_meeny:cookies:smoke_test[shadow,2]`
113
+
114
+ You can add the resulting cookie to your browser by copying the cookie string and use the following command in the JS console of your browser.
115
+
116
+ ```
117
+ document.cookie = '<cookie string excluding httponly>';
118
+ ```
119
+
120
+ Please note that the `HttpOnly` attribute will prevent you from adding the cookie to your browser through JS. You will therefor have to remove the `HttpOnly` part of the cookie string before adding the cookie to your browser.
121
+
122
+ Setting up Experiments
123
+ -------------
124
+ It is easiest to define your experiments in YAML files and load them with as shown in the **Configuration** section.
125
+
126
+ When setting up a new experiment you need to provide the following information:
127
+
128
+ * `experiment_id` This is the key that encapsulates the rest of your experiment configuration in the YAML file (see `:experiment_1:` the **Configuration** section).
129
+ * `name` The name/title of your experiment.
130
+ * `version` (optional) the version of your experiment. Defaults to `1`.
131
+ * `start_at` (optional) the start time of your experiment. Will enable the experiment at the given time.
132
+ * `end_at` (optional) the end time of your experiment. Will disable the experiment at the given time.
133
+ * `variations` The set of variations to be included in your experiment (see options for variations below).
134
+
135
+ A variation needs the following information:
136
+
137
+ * `variation_id` This is the key that encapsulates the rest of your variation configuration in the YAML file (see `:a:` the **Configuration** section).
138
+ * `name` The name/title of your varition.
139
+ * `weight` The weight of the variation. Defaults to `1`. This can be a floating or integer number. The final weight of the variation will be `weight / sum_of_variation_weights`.
140
+ * `options` (optional) a hash with variation specific information that you want stored want to use in your experiment. Notice that this information will be stored in the experiment cookie so avoid putting sensitive data in there - especially if you choose to disable the cookie encryption.
141
+
142
+ If you want to force all your users to get their experiment cookie updated, then you can change the `version` option on your experiment. This might for instance be useful if you want to remove an under-performing variation from your experiment. Or when gradually rolling a feature out to the public.
143
+
144
+ Split testing
145
+ -------------
146
+ The `eeny-meeny` gem can be used to split test features in your Rails application. The goal of split testing is to test a new feature/design/component on your website on a selected fraction of the website sessions and measure its performance.
147
+
148
+ With `eeny-meeny` you are able to testing multiple variations and select the weight of each experiment variation. This allows you to fine-tune your experiment and gradually roll a change out to your users.
149
+
150
+ If you want to render a different partial as for users that participates in a specific variation of your experiment, then you can use the `participates_in?(experiment_id, variation_id: variation_id)` helper in your view.
151
+
152
+ ```
153
+ ...
154
+ %div.content
155
+ - if participates_in?(:my_experiment, variation_id: :my_variation)
156
+ = render 'my_variation_partial'
157
+ - else
158
+ = render 'normal_partial'
159
+ ```
160
+
161
+ If you want to do slightly different things in your controller for users that participates in a variation of your experiment, then you can do as follows:
162
+
163
+ ```
164
+ def show
165
+ if participates_in?(:my_experiment, variation_id: :my_variation)
166
+ # variation specific code
167
+ else
168
+ # normal code
169
+ end
170
+ end
171
+ ```
172
+
173
+ It is also possible to use `participates_in?(:my_experiment)` to get the variation that the user participates in; and possible show the data stored in the experiment variation.
51
174
 
52
175
  Full page split tests
53
176
  -------------
@@ -68,17 +191,60 @@ If you want to completely redesign a page but test it in production as a split t
68
191
 
69
192
  2. Namespace your controller and views (ex. `ExamplesController` becommes `V1::ExamplesController` )
70
193
  3. Copy the route(s) for `ExamplesController` and use `controller: 'v1/examples`
71
- 4. Add `require 'eeny-meeny/route_constraint` to `routes.rb`
194
+ 4. Add `require 'eeny-meeny/routing/experiment_constraint` to `routes.rb`
72
195
  4. Surround your `v1/examples` route(s) with the following constraint:
73
196
 
74
197
  ```
75
- constraints(EenyMeeny::RouteConstraint.new(:example_page, variation_id: :v1) do
198
+ constraints(EenyMeeny::ExperimentConstraint.new(:example_page, variation_id: :v1) do
76
199
  # your v1 routes goes here.
77
200
  end
78
201
  ```
79
202
 
80
203
  Now 90% of the users will experience V1 and 10% will experience V2.
81
204
 
205
+ Measuring your results
206
+ -------------
207
+ `eeny-meeny` leaves the choice of how to measure your results entirely in your hands.
208
+
209
+ If you track via Google Analytics (GA) or Google Tag Manager (GTM) then add markup / js events to the pages you render and measure the results in the Google Analytics interface.
210
+
211
+ If you prefer to track results / conversions in a different system then use the `participates_in?` helper to learn the user variation and send an event to your preferred analytics tool from your controller or from JS in the browser.
212
+
213
+ Smoke testing
214
+ -------------
215
+ The `eeny-meeny` gem can be used to let you test features in production without your users seeing them. It also allows you to keep hidden features in production that only will be available to the selected few that have a valid encrypted smoke test cookie in their browser.
216
+
217
+ If you want a specific route to only be available as a smoke test, then you can add the following route.
218
+
219
+ ```
220
+ constraints(EenyMeeny::SmokeTestConstraint.new(:shadow_test) do
221
+ # your 'shadow test' routes
222
+ end
223
+ ```
224
+
225
+ If you want to render a different partial as a smoke test, then you can use the `smoke_test?(smoke_test_id)` helper in your view.
226
+
227
+ ```
228
+ ...
229
+ %div.content
230
+ - if smoke_test?(:shadow_test)
231
+ = render 'smoke_test_partial'
232
+ - else
233
+ = render 'normal_partial'
234
+ ```
235
+
236
+ If you want to do slightly different things in your controller as a smoke test, then you can do as follows:
237
+
238
+ ```
239
+ def show
240
+ if smoke_test?(:shadow_test)
241
+ # smoke test specific code
242
+ else
243
+ # normal code
244
+ end
245
+ end
246
+ ```
247
+
82
248
  Special thanks
83
249
  -------------
84
250
  As part of building this gem I borrowed the `Encryptor` class from the `encrypted_cookie` gem (https://github.com/cvonkleist/encrypted_cookie)
data/eeny-meeny.gemspec CHANGED
@@ -3,8 +3,8 @@ require File.expand_path('../lib/eeny-meeny/version', __FILE__)
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'eeny-meeny'
5
5
  s.version = EenyMeeny::VERSION.dup
6
- s.date = '2016-07-03'
7
- s.summary = "A simple split testing tool for Rails"
6
+ s.date = '2016-09-11'
7
+ s.summary = "A simple split and smoke testing tool for Rails"
8
8
  s.authors = ["Christian Orthmann"]
9
9
  s.email = 'christian.orthmann@gmail.com'
10
10
  s.require_path = 'lib'
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.add_development_dependency('simplecov', '~> 0')
19
19
  s.add_development_dependency('simplecov-rcov', '~> 0')
20
20
  s.add_development_dependency('yard', '~> 0')
21
+ s.add_development_dependency('rack-test', '~> 0.6.3')
21
22
  s.add_runtime_dependency('rack', '>= 1.2.1', '< 2')
22
23
  s.add_runtime_dependency('activesupport', '>= 3.0.0', '< 5.0.0')
23
24
  end
data/lib/eeny-meeny.rb CHANGED
@@ -1,6 +1,33 @@
1
1
  require 'eeny-meeny/version'
2
2
  require 'eeny-meeny/railtie' if defined?(Rails)
3
+ require 'eeny-meeny/models/encryptor'
3
4
 
4
5
  module EenyMeeny
5
- EENY_MEENY_COOKIE_PREFIX = 'eeny_meeny_'.freeze
6
+
7
+ class Config
8
+ attr_accessor :cookies, :experiments, :secret, :secure
9
+
10
+ attr_reader :encryptor
11
+
12
+ def initialize
13
+ @cookies = { http_only: true, path: '/', same_site: :strict }
14
+ @experiments = {}
15
+ @secret = '9fc8b966eca7d03d55df40c01c10b8e02bf1f9d12d27b8968d53eb53e8c239902d00bf6afae5e726ce1111159eeb2f8f0e77233405db1d82dd71397f651a0a4f'
16
+ @secure = true
17
+ @encryptor = (@secure ? EenyMeeny::Encryptor.new(@secret) : nil)
18
+ end
19
+ end
20
+
21
+ def self.config
22
+ @@config ||= Config.new
23
+ end
24
+
25
+ def self.configure
26
+ yield self.config
27
+ end
28
+
29
+ def self.reset!
30
+ @@config = nil
31
+ end
32
+
6
33
  end
@@ -1,21 +1,23 @@
1
- require 'eeny-meeny/shared_methods'
1
+ require 'eeny-meeny/models/cookie'
2
+ require 'eeny-meeny/models/experiment'
2
3
 
3
4
  module EenyMeeny::ExperimentHelper
4
- @@eeny_meeny_encryptor = nil
5
5
 
6
6
  def participates_in?(experiment_id, variation_id: nil)
7
- cookie = eeny_meeny_cookie(experiment_id)
7
+ experiment = EenyMeeny::Experiment.find_by_id(experiment_id)
8
+ return unless experiment.active?
9
+ cookie = read_cookie(EenyMeeny::Cookie.cookie_name(experiment))
8
10
  cookie[:variation] unless cookie.nil? || (variation_id.present? && variation_id != cookie[:variation].id)
9
11
  end
10
12
 
13
+ def smoke_test?(smoke_test_id, version: 1)
14
+ cookie = read_cookie(EenyMeeny::Cookie.smoke_test_name(smoke_test_id, version: version))
15
+ cookie unless cookie.nil?
16
+ end
17
+
11
18
  private
12
19
 
13
- def eeny_meeny_cookie(experiment_id)
14
- cookie = cookies[EenyMeeny::EENY_MEENY_COOKIE_PREFIX+experiment_id.to_s+'_v'+experiment_version(experiment_id).to_s]
15
- if cookie
16
- Marshal.load(decrypt(cookie)) rescue nil
17
- end
20
+ def read_cookie(cookie_name)
21
+ EenyMeeny::Cookie.read(cookies[cookie_name])
18
22
  end
19
-
20
- include EenyMeeny::SharedMethods
21
23
  end
@@ -1,22 +1,17 @@
1
1
  require 'rack'
2
2
  require 'time'
3
3
  require 'active_support/time'
4
- require 'eeny-meeny/middleware_helper'
5
- require 'eeny-meeny/experiment'
6
- require 'eeny-meeny/encryptor'
4
+ require 'eeny-meeny/models/experiment'
5
+ require 'eeny-meeny/models/encryptor'
6
+ require 'eeny-meeny/models/cookie'
7
7
 
8
8
  module EenyMeeny
9
9
  class Middleware
10
- include EenyMeeny::MiddlewareHelper
11
10
 
12
- def initialize(app, experiments, secure, secret, cookie_path, cookie_same_site)
11
+ def initialize(app)
13
12
  @app = app
14
- @experiments = experiments.map do |id, experiment|
15
- EenyMeeny::Experiment.new(id, **experiment)
16
- end
17
- @secure = secure
18
- @cookie_config = { path: cookie_path, same_site: cookie_same_site }
19
- @encryptor = EenyMeeny::Encryptor.new(secret) if secure
13
+ @experiments = EenyMeeny::Experiment.find_all
14
+ @cookie_config = EenyMeeny.config.cookies
20
15
  end
21
16
 
22
17
  def call(env)
@@ -27,22 +22,20 @@ module EenyMeeny
27
22
  existing_set_cookie_header = env['Set-Cookie']
28
23
  # Prepare for experiments.
29
24
  @experiments.each do |experiment|
30
- # Skip experiments that haven't started yet or if it ended
31
- next if experiment.start_at && (now < experiment.start_at)
32
- next if experiment.end_at && (now > experiment.end_at)
25
+ # Skip inactive experiments
26
+ next unless experiment.active?(now)
33
27
  # skip experiments that already have a cookie
34
- unless has_experiment_cookie?(cookies, experiment)
28
+ unless cookies.has_key?(EenyMeeny::Cookie.cookie_name(experiment))
35
29
  env['Set-Cookie'] = ''
36
- cookie_value = generate_cookie_value(experiment, @cookie_config)
37
- cookie_value[:value] = @encryptor.encrypt(cookie_value[:value]) if @secure
30
+ cookie = EenyMeeny::Cookie.create_for_experiment(experiment, @cookie_config)
38
31
  # Set HTTP_COOKIE header to enable experiment on first pageview
39
32
  Rack::Utils.set_cookie_header!(env,
40
- experiment_cookie_name(experiment),
41
- cookie_value)
33
+ cookie.name,
34
+ cookie.to_h)
42
35
  env['HTTP_COOKIE'] = '' if env['HTTP_COOKIE'].nil?
43
36
  env['HTTP_COOKIE'] += '; ' unless env['HTTP_COOKIE'].empty?
44
37
  env['HTTP_COOKIE'] += env['Set-Cookie']
45
- new_cookies[experiment_cookie_name(experiment)] = cookie_value
38
+ new_cookies[cookie.name] = cookie
46
39
  end
47
40
  end
48
41
  # Clean up 'Set-Cookie' header.
@@ -56,7 +49,7 @@ module EenyMeeny
56
49
  response = Rack::Response.new(body, status, headers)
57
50
  # Add new cookies to 'Set-Cookie' header
58
51
  new_cookies.each do |key, value|
59
- response.set_cookie(key,value)
52
+ response.set_cookie(key,value.to_h)
60
53
  end
61
54
  response.finish
62
55
  end
@@ -0,0 +1,104 @@
1
+ require 'rack'
2
+
3
+ module EenyMeeny
4
+ class Cookie
5
+ COOKIE_PREFIX = 'eeny_meeny_'.freeze
6
+ SMOKE_TEST_PREFIX = 'smoke_test_'.freeze
7
+
8
+ attr_accessor :value
9
+ attr_reader :name, :expires, :httponly, :same_site, :path
10
+
11
+ def self.create_for_experiment_variation(experiment, variation_id, config = {})
12
+ variation = experiment.variations.detect { |variation| variation.id == variation_id }
13
+ raise "Variation '#{variation_id}' not found for Experiment '#{experiment.id}'" if variation.nil?
14
+ options = {
15
+ name: cookie_name(experiment),
16
+ value: Marshal.dump({
17
+ name: experiment.name,
18
+ variation: variation
19
+ })
20
+ }
21
+ options[:expires] = experiment.end_at if experiment.end_at
22
+ if EenyMeeny.config.secure
23
+ options[:value] = EenyMeeny.config.encryptor.encrypt(options[:value])
24
+ end
25
+ new(**options.merge(config))
26
+ end
27
+
28
+ def self.create_for_experiment(experiment, config = {})
29
+ options = {
30
+ name: cookie_name(experiment),
31
+ value: Marshal.dump({
32
+ name: experiment.name,
33
+ variation: experiment.pick_variation
34
+ })
35
+ }
36
+ options[:expires] = experiment.end_at if experiment.end_at
37
+ if EenyMeeny.config.secure
38
+ options[:value] = EenyMeeny.config.encryptor.encrypt(options[:value])
39
+ end
40
+ new(**options.merge(config))
41
+ end
42
+
43
+ def self.create_for_smoke_test(smoke_test_id, version: 1, **config)
44
+ options = {
45
+ name: smoke_test_name(smoke_test_id, version: version),
46
+ value: Marshal.dump({
47
+ name: smoke_test_id,
48
+ version: version
49
+ })
50
+ }
51
+ if EenyMeeny.config.secure
52
+ options[:value] = EenyMeeny.config.encryptor.encrypt(options[:value])
53
+ end
54
+ new(**options.merge(config))
55
+ end
56
+
57
+ def self.smoke_test_name(smoke_test_id, version: 1)
58
+ return if smoke_test_id.nil?
59
+ SMOKE_TEST_PREFIX+smoke_test_id.to_s+'_v'+version.to_s
60
+ end
61
+
62
+ def self.cookie_name(experiment)
63
+ return if experiment.nil?
64
+ COOKIE_PREFIX+experiment.id.to_s+'_v'+experiment.version.to_s
65
+ end
66
+
67
+ def self.read(cookie_string)
68
+ return if cookie_string.nil? || cookie_string.empty?
69
+ begin
70
+ return Marshal.load(cookie_string) unless EenyMeeny.config.secure # Cookie encryption disabled.
71
+ Marshal.load(EenyMeeny.config.encryptor.decrypt(cookie_string))
72
+ rescue
73
+ nil # Return nil if cookie is invalid.
74
+ end
75
+ end
76
+
77
+ def initialize(name: '', value: '', expires: 1.month.from_now, http_only: true, same_site: nil, path: nil)
78
+ @name = name
79
+ @expires = expires
80
+ @httponly = http_only
81
+ @value = value
82
+ @same_site = same_site
83
+ @path = path
84
+ end
85
+
86
+ def to_h
87
+ hash = {
88
+ expires: @expires,
89
+ httponly: @httponly,
90
+ value: @value
91
+ }
92
+ hash[:same_site] = @same_site unless @same_site.nil?
93
+ hash[:path] = @path unless @path.nil?
94
+ hash
95
+ end
96
+
97
+ def to_s
98
+ header = {}
99
+ Rack::Utils.set_cookie_header!(header, name, to_h)
100
+ header['Set-Cookie']
101
+ end
102
+
103
+ end
104
+ end