Skip to main content

Ruby

View the full source code at https://github.com/growthbook/growthbook-ruby.

Requirements

The Ruby SDK requires Ruby version 2.5.0 or higher.

Installation

Install the gem:

gem install growthbook

Quick start

require 'growthbook'

# Fetch features from a GrowthBook instance
# You should cache this in Redis or similar in production
features_repository = Growthbook::FeatureRepository.new(
endpoint: 'https://cdn.growthbook.io/api/features/MY_API_KEY',
decryption_key: nil
)
features = features_repository.fetch

# Create a context for the current user/request
gb = Growthbook::Context.new(
features: features,
# User attributes for targeting / variation assignment
attributes: {
id: '123',
country: 'US'
}
)

# Use a boolean feature flag
if gb.on? :my_feature_key
puts 'My feature is on!'
end

# Get the value of a multivariate feature with a fallback
btn_color = gb.feature_value(:signup_btn_color, 'pink')

Tracking

Track experiment impressions

When a feature's value is determined by an experiment (A/B test), you typically want to track that assignment event for later analysis.

There are two ways to do this. First is by accessing all impressions at the end of a request:

gb.impressions.each do |key, result|
puts "Assigned variation #{result.variation_id} in experiment #{key}"
end

Second is by using a listener to get alerted in realtime as users are put into experiments:

class MyImpressionListener
def on_experiment_viewed(experiment, result)
puts "Assigned variation #{result.variation_id} in experiment #{experiment.key}"
end
end

gb.listener = MyImpressionListener.new

Track feature usage

GrowthBook can fire a callback whenever a feature is evaluated for a user. This can be useful to update 3rd party tools like NewRelic or DataDog.

Provide a receiver that can receive def on_feature_usage: (String _feature_key, FeatureResult _result) -> void. There's a convenience class FeatureUsageCallback with a method you can override but you can provide your own.

class MyFeatureUsageCallback < FeatureUsageCallback
def on_feature_usage(feature_key, feature_result)
puts "on_feature_usage_called with key: #{feature_key} and result #{feature_result}"
end
end

on_feature_usage = MyFeatureUsageCallback.new

# you can pass it into the context
gb = Growthbook::Context.new({
attributes: {
id: 'user-abc123'
},
features: feature_repository.fetch || {},
on_feature_usage: on_feature_usage,
})

# or assign it afterwards
gb.on_feature_usage = on_feature_usage

Using with Rails

You can use the provided Growthbook::FeatureRepository class along with the Rails cache to fetch features periodically within your usage limits. Here is a controller concern you can use:

require 'growthbook'

module GrowthbookSdk
def growthbook
@growthbook ||= Growthbook::Context.new(
features: growthbook_features_json,
attributes: {},
)
end

# use this as a before_action on your controller
def init_feature_flags
return if current_user.nil?

# TODO: Change this to get your user attributes as a hash in a way that works for your app
growthbook.attributes = current_user.as_json
end

private

def growthbook_features_json
Rails.cache.fetch("growthbook_features", expires_in: 1.hour) do
puts "🌎 Fetching GrowthBook features from the network"

repo = Growthbook::FeatureRepository.new(
endpoint: 'https://cdn.growthbook.io/api/features/java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8',
decryption_key: nil,
)

repo.fetch || {}
end
end
end

And in your ApplicationController:

class ApplicationController < ActionController::API
include Authentication # your own auth strategy
include GrowthbookSdk # the controller concern code above

before_action :authenticate!
before_action :init_feature_flags # call this once you have a user from which to get attributes
end

The above code exposes the following methods on your application controller:

  • growthbook: an instance of the GrowthBook SDK for the request
  • init_feature_flags: a method intended to be used as a before_action hook, e.g. before_action :init_feature_flags

It assumes you have a method current_user that returns the currently-authenticated user, and that it responds to as_json to return a hash of the targeting attributes.

How this works:

  1. With each request, the init_feature_flags method is called. This creates a new instance of Growthbook::Context
  2. When creating the context for the first time, features are fetched and cached in the Rails cache. Subsequent calls use the cached version until the cache expires.
  3. Developers can call methods on growthbook in their controllers to use the GrowthBook SDK, e.g. growthbook.on?(:dark_mode).

You can see the Rails example linked in the Code examples below.

Dev and QA helpers

For dev/QA it's often useful to force specific feature values.

# These take precedence over everything else when determining a feature's value
gb.forced_features = {
my_feature: true,
other_feature: "new value"
}

# Will always be true
gb.is_on?(:my_feature)

# Will always be "new value"
gb.feature_value(:other_feature)

For more predictability during QA, you can also globally disable all random assignment in experiments from running:

gb.enabled = false

Sticky Bucketing

Available starting in version 1.3.0

By default GrowthBook does not persist assigned experiment variations for a user. We rely on deterministic hashing to ensure that the same user attributes always map to the same experiment variation. However, there are cases where this isn't good enough. For example, if you change targeting conditions in the middle of an experiment, users may stop being shown a variation even if they were previously bucketed into it.

Sticky Bucketing is a solution to these issues. You can provide a Sticky Bucket Service to the GrowthBook instance to persist previously seen variations and ensure that the user experience remains consistent for your users.

A sample InMemoryStickyBucketService implementation is provided for reference, but in production you will definitely want to implement your own version using a database, cookies, or similar for persistence.

Sticky Bucket documents contain three fields

  • attributeName - The name of the attribute used to identify the user (e.g. id, cookie_id, etc.)
  • attributeValue - The value of the attribute (e.g. 123)
  • assignments - A hash of persisted experiment assignments. For example: {"exp1__0":"control"}

The attributeName/attributeValue combo is the primary key.

Here's an example implementation using a theoretical db object:

require 'growthbook'

class MyStickyBucketService < Growthbook::StickyBucketService
def get_assignments(attribute_name, attribute_value)
db.find({
attributeName: attribute_name,
attributeValue: attribute_value
})
end

def save_assignments(doc)
# Insert new record if not exists, otherwise update
db.upsert({
attributeName: doc["attributeName"],
attributeValue: doc["attributeValue"]
}, {
"$set": {
assignments: doc["assignments"]
}
})
end
end

# Pass in an instance of this service to your GrowthBook constructor
gb = Growthbook::Context.new(
sticky_bucket_service: MyStickyBucketService.new
)

Inline experiments

It's also possible to directly run an experiment directly in code without going through a feature flag.

# Simple 50/50 experiment
result = gb.run(Growthbook::InlineExperiment.new(
key: "my-experiment-key",
variations: ["red", "green"]
))

# Whether or not the user was included in the experiment (either true or false)
puts(result.in_experiment ? 'included' : 'excluded')

# The value of the assigned variation (either "red" or "green")
puts(result.value)

# The variation index (either 0 or 1)
puts(result.variation_id)

There are lots of additional options when running inline experiments:

gb.run(Growthbook::InlineExperiment.new(
key: "my-experiment-key",
variations: ["red", "green"],
# Filter by context attributes
condition: {
country: {
"$in": ["US", "CA"]
}
},
# Adjust variation weights from the default 50/50 split
weights: [0.8, 0.2],
# Run for a subset of traffic (0 to 1, default = 1)
coverage: 0.5,
# Use a different context attribute for assigning a variation (default = "id")
hash_attribute: "device_id",
# Use a namespace to run mutually exclusive experiments
namespace: ["pricing-page", 0, 0.25]
))

Working with Encrypted features

You can learn more about SDK Connection Endpoint Encryption.

Create a GrowthBook::Context with an encrypted payload and a decryption key:

# TODO: Replace these values with your own:
Growthbook::Context.new(
encrypted_features: 'm5ylFM6ndyOJA2OPadubkw==.Uu7ViqgKEt/dWvCyhI46q088PkAEJbnXKf3KPZjf9IEQQ+A8fojNoxw4wIbPX3aj',
decryption_key: 'Zvwv/+uhpFDznZ6SX28Yjg==',
attributes: {
id: '456',
country: 'CA'
}
)

When fetching features from the GrowthBook SDK endpoint, the encrypted features are available on a property encryptedFeatures instead of plain text on the property features. Here's an example with networking:

uri = URI('https://cdn.growthbook.io/api/features/MY_API_KEY')
res = Net::HTTP.get_response(uri)
encrypted_features = res.is_a?(Net::HTTPSuccess) ? JSON.parse(res.body)['encryptedFeatures'] : nil

Growthbook::Context.new(
encrypted_features: encrypted_features,
decryption_key: '<key-for-decrypting>',
attributes: {
id: '456',
country: 'CA'
}
)

Code Examples

Further Reading