on_ramp 0.0.1
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 +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/README.md +95 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/on_ramp.rb +46 -0
- data/lib/on_ramp/bucket.rb +33 -0
- data/lib/on_ramp/configuration.rb +27 -0
- data/lib/on_ramp/ramp.rb +20 -0
- data/lib/on_ramp/version.rb +3 -0
- data/on_ramp.gemspec +30 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5a82ddccbd6d2b5a3ccd7bab025d17a1d607efa8defc093fc8f93388463d06f3
|
4
|
+
data.tar.gz: ef4ee283385bbfbdf5529bd8c37279697066e12e0da6b168b4fff4f3e9afc184
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a8b7a6e335b35102d17fa84a400ff7bcbcb234b8d721464d154c863d4a9601cfccbad57d36b0678583a83fad7aa965b27debfa6a30b96c5c21dd2fc53d3b7e1d
|
7
|
+
data.tar.gz: ee95d5381cbfb6b83213ca0394a5b7957a7393e96943aab2d100ddb62733fb0397b82668d49eea7126677ddf272eff1c065c3475742140f6bd0610599cb358ba
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
on_ramp (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.0.1)
|
10
|
+
coderay (1.1.2)
|
11
|
+
diff-lcs (1.3)
|
12
|
+
gem-release (2.0.3)
|
13
|
+
method_source (0.9.2)
|
14
|
+
pry (0.12.2)
|
15
|
+
coderay (~> 1.1.0)
|
16
|
+
method_source (~> 0.9.0)
|
17
|
+
rake (13.0.0)
|
18
|
+
rspec (3.8.0)
|
19
|
+
rspec-core (~> 3.8.0)
|
20
|
+
rspec-expectations (~> 3.8.0)
|
21
|
+
rspec-mocks (~> 3.8.0)
|
22
|
+
rspec-core (3.8.2)
|
23
|
+
rspec-support (~> 3.8.0)
|
24
|
+
rspec-expectations (3.8.4)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.8.0)
|
27
|
+
rspec-mocks (3.8.1)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.8.0)
|
30
|
+
rspec-support (3.8.2)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
bundler (~> 2.0)
|
37
|
+
byebug
|
38
|
+
gem-release
|
39
|
+
on_ramp!
|
40
|
+
pry
|
41
|
+
rake (~> 13.0)
|
42
|
+
rspec (~> 3.0)
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
2.1.4
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# OnRamp
|
2
|
+
|
3
|
+
This gem provides two related but distinct pieces of functionality:
|
4
|
+
|
5
|
+
1. Percentage ramp ups of experiments that are intended to test load or the like
|
6
|
+
2. Segmenting users into a/b variants for experimentation purposes
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'on_ramp'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install on_ramp
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Configure the gem in an initializer using the configure method
|
27
|
+
|
28
|
+
```
|
29
|
+
# /initializers/experiment.rb
|
30
|
+
OnRamp.configure do |config|
|
31
|
+
config.ab_experiments = "/initializers/experiments.yaml"
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
Your yaml file will look similar to the below:
|
36
|
+
```
|
37
|
+
experiment_name:
|
38
|
+
ramp: 20
|
39
|
+
variants:
|
40
|
+
control: 50
|
41
|
+
variant_a: 50
|
42
|
+
```
|
43
|
+
|
44
|
+
See below for example code.
|
45
|
+
|
46
|
+
Simple use case ramping only
|
47
|
+
---
|
48
|
+
If you want to slowly ramp out a change to a subset of users, the following code is the simplest way to do so. This is not the recommended way to run an experiment. For running an experiment see below `OnRamp.ab_variant`
|
49
|
+
```
|
50
|
+
# school.rb
|
51
|
+
|
52
|
+
def cool_name
|
53
|
+
if OnRamp.ramped?(:experiment_name, owner.id)
|
54
|
+
"COOOL" + name
|
55
|
+
else
|
56
|
+
name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
As you can see here, the `#ramped?` method will return true if the user is in the ramped up group and false if they aren't. As you adjust the ramping, more users will experience the ramped up code path.
|
61
|
+
|
62
|
+
Simple use case experiment
|
63
|
+
--
|
64
|
+
The `ab_variant` method does not necessarily return a value. It will return `nil` if the unique_id you've passed is not yet ramped up. If you have configured your experiment with 100% ramp, then `#ab_variant` will always return a value.
|
65
|
+
```
|
66
|
+
def cool_name_experiment
|
67
|
+
case OnRamp.ab_variant(:cool_name, owner.id)
|
68
|
+
when "variant_a"
|
69
|
+
"COOOL" + name
|
70
|
+
when "variant_b"
|
71
|
+
"C000L" + name
|
72
|
+
when "control"
|
73
|
+
control_group_name
|
74
|
+
else
|
75
|
+
name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
It may be common to have control and non-ramped experiences be the same, it's very easy with the case `else` clause.
|
80
|
+
```
|
81
|
+
def cool_name_final
|
82
|
+
case Experiment::ab_variant(:cool_name, owner.id)
|
83
|
+
when "variant_a"
|
84
|
+
"COOOL" + name
|
85
|
+
when "variant_b"
|
86
|
+
"C000L" + name
|
87
|
+
else
|
88
|
+
name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
## Development
|
94
|
+
|
95
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec spec/` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "on_ramp"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/on_ramp.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'on_ramp/bucket'
|
4
|
+
require 'on_ramp/configuration'
|
5
|
+
require 'on_ramp/ramp'
|
6
|
+
|
7
|
+
module OnRamp
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def ramped?(experiment_name:, unique_id:)
|
11
|
+
validate_experiment_name(experiment_name)
|
12
|
+
OnRamp::Ramp.ramped?(experiment_name.to_s, unique_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ab_variant(experiment_name:, unique_id:)
|
16
|
+
validate_experiment_name(experiment_name)
|
17
|
+
variant = if !ramped?(
|
18
|
+
experiment_name: experiment_name,
|
19
|
+
unique_id: unique_id
|
20
|
+
)
|
21
|
+
nil
|
22
|
+
else
|
23
|
+
OnRamp::Bucket.get_variant(
|
24
|
+
experiment_name: experiment_name.to_s,
|
25
|
+
unique_id: unique_id
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
OnRamp.configuration.ab_variant_callback_function&.call(
|
30
|
+
experiment_name,
|
31
|
+
variant,
|
32
|
+
unique_id
|
33
|
+
)
|
34
|
+
|
35
|
+
variant
|
36
|
+
end
|
37
|
+
|
38
|
+
class InvalidExperimentName < StandardError; end
|
39
|
+
|
40
|
+
def validate_experiment_name(experiment_name)
|
41
|
+
return if OnRamp.ab_experiments[experiment_name.to_s]
|
42
|
+
|
43
|
+
raise InvalidExperimentName,
|
44
|
+
"The key does not exist: #{experiment_name}"
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module OnRamp
|
5
|
+
module Bucket
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def get_variant(experiment_name:, unique_id:)
|
10
|
+
weight = get_weight(
|
11
|
+
unique_id: unique_id, experiment_name: experiment_name
|
12
|
+
)
|
13
|
+
|
14
|
+
running_total = 0.0
|
15
|
+
cfg = OnRamp.ab_experiments[experiment_name]['variants']
|
16
|
+
|
17
|
+
normalize_variants(cfg).each_pair do |variant_name, percentage|
|
18
|
+
running_total += percentage
|
19
|
+
return variant_name if weight <= running_total
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_weight(unique_id:, experiment_name:, version: nil)
|
24
|
+
md5 = ::Digest::MD5.hexdigest("#{unique_id}-#{experiment_name}-#{version}")
|
25
|
+
md5[0...8].to_i(16).to_f / 0xFFFFFFFF
|
26
|
+
end
|
27
|
+
|
28
|
+
def normalize_variants(variants_cfg)
|
29
|
+
variants_cfg.map { |k, v| [k, v / 100.0] }.to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module OnRamp
|
4
|
+
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
yield(configuration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ab_experiments
|
16
|
+
configuration.ab_experiments
|
17
|
+
end
|
18
|
+
|
19
|
+
class Configuration
|
20
|
+
attr_reader :ab_experiments, :ab_variant_callback_function
|
21
|
+
|
22
|
+
def ab_experiments=(path)
|
23
|
+
@ab_experiments = YAML.safe_load(File.open(path).read)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/on_ramp/ramp.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module OnRamp
|
4
|
+
module Ramp
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def ramped?(experiment_name, unique_id)
|
9
|
+
ramp_threshold = OnRamp.ab_experiments[experiment_name.to_s]\
|
10
|
+
['ramp'] / 100.0
|
11
|
+
weight = OnRamp::Bucket.get_weight(
|
12
|
+
unique_id: unique_id,
|
13
|
+
experiment_name: experiment_name,
|
14
|
+
version: 'ramp'
|
15
|
+
)
|
16
|
+
|
17
|
+
ramp_threshold > weight
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/on_ramp.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "on_ramp/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "on_ramp"
|
7
|
+
spec.version = OnRamp::VERSION
|
8
|
+
spec.authors = ["Michael Sterling", "Rhoen Pruesse-Adams", "James Lee"]
|
9
|
+
spec.email = ["michael@teachable.com", "rhoen@teachable.com", "james@teachable.com"]
|
10
|
+
|
11
|
+
spec.summary = "An internal Teachable library for ramping up experiments incrementally and segmenting users in A/B tests"
|
12
|
+
|
13
|
+
|
14
|
+
# Specify which files should be added to the gem when it is released.
|
15
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
16
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|experiments)/}) }
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
# development dependencies
|
24
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
25
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
27
|
+
spec.add_development_dependency "pry"
|
28
|
+
spec.add_development_dependency "byebug"
|
29
|
+
spec.add_development_dependency "gem-release"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: on_ramp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Sterling
|
8
|
+
- Rhoen Pruesse-Adams
|
9
|
+
- James Lee
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2020-03-26 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: bundler
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - "~>"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '2.0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rake
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '13.0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '13.0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rspec
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '3.0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '3.0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: pry
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: byebug
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: gem-release
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
description:
|
100
|
+
email:
|
101
|
+
- michael@teachable.com
|
102
|
+
- rhoen@teachable.com
|
103
|
+
- james@teachable.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- Gemfile
|
109
|
+
- Gemfile.lock
|
110
|
+
- README.md
|
111
|
+
- bin/console
|
112
|
+
- bin/setup
|
113
|
+
- lib/on_ramp.rb
|
114
|
+
- lib/on_ramp/bucket.rb
|
115
|
+
- lib/on_ramp/configuration.rb
|
116
|
+
- lib/on_ramp/ramp.rb
|
117
|
+
- lib/on_ramp/version.rb
|
118
|
+
- on_ramp.gemspec
|
119
|
+
homepage:
|
120
|
+
licenses: []
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubygems_version: 3.0.6
|
138
|
+
signing_key:
|
139
|
+
specification_version: 4
|
140
|
+
summary: An internal Teachable library for ramping up experiments incrementally and
|
141
|
+
segmenting users in A/B tests
|
142
|
+
test_files: []
|