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 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
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in on_ramp.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module OnRamp
2
+ VERSION = '0.0.1'
3
+ 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: []