Small utility library to run controlled experiments (i.e. AB tests) in Ruby. Comaptible with the Growth Book experimentation platform.
Install the gem
gem install growthbook
require 'growthbook'
# Do this once during app bootup
client = Growthbook::Client.new
# Logged-in id of the user being experimented on
# Can also use an anonymous id like session (see below)
user = client.user(id: "12345")
# 2 variations, 50/50 split
exp = Growthbook::Experiment.new("experiment-id", 2)
result = user.experiment(exp)
case result.variation
when 0
puts "Control"
when 1
puts "Variation"
else
puts "Not in experiment"
endThe Growthbook::Client constructor takes an optional config hash.
Currently, the only option is enabled which defaults to true. When false, all experiments are completely disabled.
client = Growthbook::Config.new(enabled: false)
# All user.experiment calls will now return -1Config options are public instance variables and you can change them any time:
client.enabled=trueThe client.user method takes a single hash with a few possible keys:
id- The logged-in user idanonId- An anonymous identifier for the user (session id, cookie, ip, etc.)attributes- A hash with user attributes. These are never sent across the network and are only used to locally evaluate experiment targeting rules.
Although all of these are technically optional, at least 1 type of id must be set or the user will be excluded from all experiments.
Here is an example that uses all 3 properties:
user = client.user(
# Logged-in user id
id: "12345",
# Anonymous id
anonId: "abcdef",
# Targeting attributes
attributes: {
:premium => true,
:accountAge => 36,
:geo => [
:region => "NY"
]
}
])You can update these at any time:
user.id="54321"
user.anonId = "fedcba"
user.attributes={
:premium => false
}The default test is a 50/50 split with no targeting or customization. There are a few ways to configure this on a test-by-test basis.
With this option, you configure all experiments globally once and then reference them via id throughout the code.
client.experiments=[
# Default 50/50 2-way test
Growthbook::Experiment.new("my-test", 2),
# 3-way test with reduced coverage and unequal weights
Growthbook::Experiment.new(
"my-other-test",
3,
:coverage => 0.4,
:weights => [0.5, 0.25, 0.25]
)
]
# Later in code, pass the string id instead of the Experiment object
result = user.experiment("my-test")There is a helper method that can import a hash of multiple experiments at once. The expected format follows the GrowthBook API response.
require 'json'
# Example response from the GrowthBook API
json = '{
"status": 200,
"experiments": {
"my-test": {
"variations": 2
},
"my-other-test": {
"variations": 3,
"weights": [0.5, 0.25, 0.25]
}
}
}'
parsed = JSON.parse(json)
client.importExperimentsHash(parsed["experiments"])As shown in the quick start above, you can use a Growthbook::Experiment object directly to run an experiment.
The below example shows all of the possible experiment options you can set:
# 1st argument is the experiment id
# 2nd argument is the number of variations
experiment = Growthbook::Experiment.new("my-experiment-id", 3,
# Percent of eligible traffic to include in the test (from 0 to 1)
:coverage => 0.5,
# How to split traffic between variations (must add to 1)
:weights => [0.34, 0.33, 0.33],
# If false, use the logged-in user id to assign variations
# If true, use the anonymous id
:anon => false,
# If set to an integer, force that variation for all users in the experiment
:force => 1,
# Targeting rules
# Evaluated against user attributes to determine who is included in the test
:targeting => ["source != google"],
# Add arbitrary data to the variations (see below for more info)
:data => {
"color" => ["blue","green","red"]
}
)
result = user.experiment(experiment)Growth Book supports 3 different implementation approaches:
- Branching
- Parameterization
- Config System
This is the simplest to understand and implement. You add branching via if/else or case statements:
result = user.experiment("experiment-id")
if result.variation == 1
# Variation
buttonColor = "green"
else
# Control or Not in experiment
buttonColor = "blue"
endWith this approach, you parameterize the variations by associating them with data.
experiment = Growthbook::Experiment.new("experiment-id", 2,
:data => {
"color" => ["blue", "green"]
}
)
result = user.experiment(experiment)
# Will be either "blue" or "green"
buttonColor = result.data["color"]
# If no data is defined for the key, `nil` is returned
result.data["unknown"] == nil # trueIf you already have an existing configuration or feature flag system, you can do a deeper integration that
avoids experiment calls throughout your code base entirely.
All you need to do is modify your existing config system to get experiment overrides before falling back to your normal lookup process:
# Your existing function
def getConfig(key)
# Look for a valid matching experiment.
# If found, choose a variation and return the value for the requested key
result = user.lookupByDataKey(key)
if result != nil
return result.value
end
# Continue with your normal lookup process
...
endInstead of generic keys like color, you probably want to be more descriptive with this approach (e.g. homepage.cta.color).
With the following experiment data:
{
:data => {
"homepage.cta.color" => ["blue", "green"]
}
}You can now do:
buttonColor = getConfig("homepage.cta.color")Your code now no longer cares where the value comes from. It could be a hard-coded config value or part of an experiment. This is the cleanest approach of the 3, but it can be difficult to debug if things go wrong.
When someone is put into an experiment, you'll typically want to log this event in your analytics system.
The user object has an array resultsToTrack that stores all experiment results that should be tracked.
For example, if you are using Segment on the front-end, you can add something like this to the bottom of your HTML:
<% user.resultsToTrack.each do |result| %>
<script>
analytics.track("Experiment Viewed", {
experiment_id: "<%= result.experiment.id %>",
variation_id: <%= result.variation_id %>
})
</script>
<% end %>