Ruby Quickstart
Get Running with the Convert Ruby SDK in 5 Minutes
THIS ARTICLE WILL HELP YOU:
- Get an Overview
- Install the Convert Ruby SDK
- Understand a Full Working Example
- Know Some Key Points
- Run an Experience
- Track Conversions and Revenue
- Flush Events
- Get Rails and Ruby Server Runtime Considerations
- Get Front-End and Server-Rendered Implementation Considerations
- Know Optional Redis Store for Multi-Process Environments
- Track Control and Consent
- Understand QA Checklist
- Troubleshoot
- Conclusion
Overview
The Convert Ruby SDK lets you run Convert Full Stack experiments and feature flags from Ruby applications. It is useful when you want to decide a visitor’s experience before rendering a page, executing business logic, returning API output, or processing a backend job.
The Ruby SDK requires an SDK Key from your Convert dashboard and Ruby 3.1 or newer. The gem has zero runtime dependencies and uses only Ruby standard library components. Source: Ruby Quickstart
Convert Full Stack projects let teams experiment across both frontend and backend parts of an application, including UI, business logic, database behavior, and server-side rendering.
1. Install the Ruby SDK
Add the gem to your Gemfile:
# Gemfile
gem "convert_sdk"
Then install it:
bundle install
Or install it directly:
gem install convert_sdk
The Ruby SDK is published as convert_sdk, supports Ruby 3.1+, and has no runtime dependencies.
For production, pin a specific version instead of tracking the latest:
gem "convert_sdk", "~> 1.0"
Requirements
|
Requirement |
Details |
|
Ruby |
3.1 or newer |
|
Supported runtimes |
CRuby 3.1, 3.2, 3.3, 3.4, and JRuby |
|
Runtime dependencies |
None; standard library only |
|
Optional dependency |
redis, only if you use RedisStore |
2. Full Working Example
Use the following copy-paste-ready example to build one SDK client, create a visitor context, run an experience, branch on the result, track a purchase conversion, and flush queued events.
require "convert_sdk"
# 1. Build ONE client at boot and reuse it for the life of the process.
# Fetch mode: pass an sdk_key.
# Direct-data mode: pass pre-fetched data.
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY")
)
# 2. Create one context per visitor.
# Contexts are cheap: no network and no thread.
context = CONVERT_SDK.create_context(
"visitor-123",
{ "country" => "US" }
)
# 3. Run an experience.
# Returns a BucketedVariation on a hit, or a Sentinel on a miss.
variation = context.run_experience("homepage-test")
# 4. Act on the result.
# variation&.key is the variation key on a hit and nil on a miss.
case variation&.key
when nil
render_default
when "treatment"
render_treatment
else
render_variation(variation.key)
end
# 5. Track a conversion with revenue.
# Conversions are deduplicated per visitor per goal by default.
context.track_conversion(
"purchase",
goal_data: {
amount: 49.99,
transaction_id: "tx-1"
}
)
# 6. Flush queued events synchronously.
# Long-running servers also drain via the background timer.
# Short-lived processes such as Lambda or CLI scripts should flush before exit.
CONVERT_SDK.flush
3. Key Points
Build one client at boot
Create the SDK client once at application boot and reuse it for the life of the process:
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY")
)
Do not create a new SDK client per request. ConvertSdk.create is the single entry point, and it fetches the bucketing config synchronously. The only thing create may raise is an ArgumentError for misconfiguration.
Create one context per visitor
Create a fresh context per visitor, web request, or job:
context = CONVERT_SDK.create_context(
"visitor-123",
{ "country" => "US" }
)
A context makes no network request. The first create_context call lazily starts the background config-refresh timer, which helps with fork-safe behavior by avoiding a background thread in a preload master process and starting it only after the worker creates its first context. A blank or nil visitor ID logs an error and returns nil.
Use a stable visitor ID
Use a stable visitor identifier when creating a context. For logged-in users, this can be your internal user ID. For anonymous visitors, use a persistent first-party cookie or another stable anonymous identifier.
Stable visitor IDs help keep visitors assigned to the same variation across requests, sessions, workers, and backend jobs.
Ruby SDK decisions do not return bare nil
run_experience returns a frozen BucketedVariation on a hit or a frozen Sentinel on a business miss. It does not raise and does not return a bare nil for a business miss.
This means the recommended branching pattern is:
variation = context.run_experience("homepage-test")
case variation&.key
when nil
render_default
when "treatment"
render_treatment
else
render_variation(variation.key)
end
A sentinel’s key is always nil, so the nil branch safely handles non-bucketed visitors or other business misses.
4. Running an Experience
Use run_experience() with the experience key configured in Convert:
variation = context.run_experience("homepage-test")
Then branch on the returned variation key:
case variation&.key
when nil
render :homepage_control
when "new-hero"
render :homepage_new_hero
else
render :homepage_control
end
This is useful when the variation should be decided before the response is rendered, such as for Rails views, server-rendered HTML, API responses, pricing logic, checkout behavior, or backend-controlled onboarding flows.
5. Tracking Conversions and Revenue
Use track_conversion() to send goal completions to Convert.
Bare conversion:
context.track_conversion("signup")
Conversion with revenue and transaction data:
context.track_conversion(
"purchase",
goal_data: {
amount: 49.99,
transaction_id: "tx-1"
}
)
track_conversion(goal_key, goal_data: nil, force_multiple_transactions: false) records a conversion and is deduplicated per visitor per goal by default. Revenue and transaction data should be passed through goal_data: using snake_case keys such as amount: and transaction_id:.
Use conversion tracking for meaningful backend or app events such as purchases, signups, subscription starts, completed onboarding, trial activation, form submissions, or backend-defined milestones.
6. Flushing Events
Call flush to synchronously deliver queued events:
CONVERT_SDK.flush
In long-running servers, the background flush timer also drains queued events, so you do not need to call flush after every request. For short-lived processes such as AWS Lambda, CLI scripts, one-off jobs, or processes that may exit quickly, call flush before the process exits.
7. Rails and Ruby Server Runtime Considerations
Rails with Puma cluster
In Rails, create the SDK client in an initializer:
# config/initializers/convert_sdk.rb
require "convert_sdk"
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY"),
sdk_key_secret: ENV["CONVERT_SDK_KEY_SECRET"]
)
Then create one context per request:
# app/controllers/pricing_controller.rb
class PricingController < ApplicationController
def show
context = CONVERT_SDK.create_context(
convert_visitor_id,
{ "country" => "US" }
)
variation = context.run_experience("pricing-test")
case variation&.key
when nil
render :pricing_control
when "annual"
render :pricing_annual
else
render :pricing_control
end
context.track_conversion("view-pricing")
end
end
For a standard Puma cluster using preload_app!, the Ruby SDK is fork-safe with zero extra fork code. An optional CONVERT_SDK.postfork call can be added in on_worker_boot, but automatic fork detection handles common runtimes.
Sidekiq
Sidekiq is threaded and single-process in its open-source mode. Build one SDK client at boot, reuse it across job threads, and flush on shutdown:
# config/initializers/convert_sdk.rb
require "convert_sdk"
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY")
)
class ConversionJob
include Sidekiq::Job
def perform(visitor_id, attributes = {})
context = CONVERT_SDK.create_context(visitor_id, attributes)
context.run_experience("homepage-test")
context.track_conversion("signup")
end
end
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.on(:shutdown) { CONVERT_SDK.flush }
end
CONVERT_SDK.flush drains the queue synchronously so in-flight events from job threads are delivered before the worker terminates.
AWS Lambda
For AWS Lambda, disable background timers and flush synchronously before the handler returns:
# handler.rb
require "convert_sdk"
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV["CONVERT_SDK_KEY"],
data_refresh_interval: nil,
flush_interval: nil
)
def handler(event:, context:)
ctx = CONVERT_SDK.create_context(event["visitorId"])
variation = ctx.run_experience("homepage-test")
CONVERT_SDK.flush
{ variation: variation.key }
end
Lambda freezes the execution environment between invocations, so the reliable delivery point is a synchronous flush before the handler returns.
8. Front-End and Server-Rendered Implementation Considerations
The Ruby SDK makes experiment decisions on the backend. In many Ruby applications, especially Rails applications, that decision affects the frontend because it determines which HTML, view, partial, JavaScript flag, or API payload the visitor receives.
Pattern A: Server-Side Rendering Only
Use this approach when Ruby controls the rendered page.
variation = context.run_experience("homepage-test")
case variation&.key
when "new-hero"
render :homepage_new_hero
else
render :homepage_control
end
In this setup:
- Ruby decides the variation before the page is sent to the browser.
- The visitor receives the correct HTML immediately.
- The frontend does not need to re-decide the variation.
- Flicker is reduced because the browser does not need to wait for client-side DOM changes.
Full Stack testing supports experimentation across backend and frontend layers, including server-side business logic and user-facing output.
Pattern B: Backend Decision + Front-End JavaScript Behavior
Use this when Ruby decides the variation, but frontend JavaScript still needs to know which variation was assigned.
Example Rails view:
<% variation = context.run_experience("homepage-test") %>
<% variation_key = variation&.key || "control" %>
<script>
window.convertAssignedVariation = <%= variation_key.to_json.html_safe %>;
</script>
Then your frontend script can read the assignment:
<script>
if (window.convertAssignedVariation === "new-hero") {
document.documentElement.classList.add("convert-new-hero");
}
</script>
This is useful when:
- Rails renders the initial HTML.
- JavaScript controls interactive components.
- A React, Vue, Stimulus, or Turbo layer needs the assigned variation.
- Analytics tools need frontend-visible experiment metadata.
- The backend controls eligibility, but the browser controls UI behavior.
Keep one source of truth for assignment. If Ruby decides the variation, pass that decision to the frontend instead of re-bucketing the visitor separately in browser code.
Pattern C: API Response Controls Front-End App Behavior
Use this when Ruby powers an API used by a frontend app.
variation = context.run_experience("checkout-flow")
render json: {
checkout_experience: variation&.key || "control",
show_express_checkout: variation&.key == "express"
}
The frontend can then render based on the API response:
if (response.show_express_checkout) {
renderExpressCheckout();
} else {
renderStandardCheckout();
}
This is useful for headless Rails apps, mobile backends, API-driven SPAs, and applications where the frontend needs server-approved feature or experiment decisions.
Avoid Duplicate Bucketing
Do not run the same experiment independently in Ruby and browser JavaScript unless the implementation is intentionally designed for that.
To avoid inconsistent results:
- Use the same stable visitor ID.
- Keep one source of truth for variation assignment.
- Pass the Ruby-assigned variation to the frontend when JavaScript needs it.
- Avoid creating multiple SDK clients per request.
- Use a shared store such as Redis for multi-process environments when sticky bucketing and goal deduplication need to be shared across processes.
The Ruby SDK defaults to an in-process MemoryStore; for multi-process fleets where sticky bucketing and goal deduplication must be shared across processes, the SDK includes ConvertSdk::Stores::RedisStore.
9. Optional Redis Store for Multi-Process Environments
For multi-process Ruby deployments, you may want sticky bucketing and goal deduplication shared across processes. Add Redis only if you use RedisStore with connection options:
# Gemfile
gem "redis"
Then configure the SDK with Redis:
require "convert_sdk"
store = ConvertSdk::Stores::RedisStore.new(
url: ENV.fetch("REDIS_URL")
)
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY"),
store: store
)
The SDK itself does not require Redis. The redis gem is only your dependency if you use RedisStore in a way that requires the SDK to build its own Redis client.
10. Tracking Control and Consent
The Ruby SDK supports two tracking controls:
- A global tracking: switch
- A per-call enable_tracking: override
Use the global switch when consent is denied or unknown for the entire client:
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY"),
tracking: false
)
When tracking: false, decisioning still runs, rule evaluation still works, sticky store data still persists, but outbound tracking events are not enqueued.
Use the per-call override when only a specific decision call should be silent:
variation = context.run_experience(
"homepage-test",
{ enable_tracking: false }
)
With enable_tracking: false, bucketing and sticky persistence still happen, but outbound event enqueue is skipped for that call.
The global switch always wins. If tracking: false is set globally, delivery is suppressed even if a call passes enable_tracking: true.
11. QA Checklist
Before launching your Ruby SDK implementation, verify the following:
- The convert_sdk gem is installed.
- Ruby version is 3.1 or newer.
- A specific gem version is pinned in production.
- One SDK client is created at boot and reused.
- A new context is created per visitor, request, or job.
- Visitor IDs are stable across requests and login states.
- Experience keys match the keys configured in Convert.
- variation&.key branching handles sentinel misses.
- Conversion goal keys match the goals configured in Convert.
- Revenue and transaction IDs are passed under goal_data:.
- flush is called before short-lived processes exit.
- Sidekiq shutdown hooks flush queued events.
- Lambda handlers disable timers and flush before returning.
- Ruby-assigned variation keys are passed to frontend JavaScript when needed.
- Browser code does not re-bucket the same experiment with a different visitor ID.
- RedisStore is considered for multi-process shared stickiness and deduplication.
- TRACE logging is enabled during QA.
12. Troubleshooting
Decisions always fall through to the default branch
Ruby SDK business misses are represented by sentinels, not exceptions or bare nil. Since sentinel key is always nil, the default branch will run when the visitor is not bucketed or no data is available.
variation = context.run_experience("homepage-test")
case variation&.key
when nil
render_default
else
render_variation(variation.key)
end
Turn on TRACE logging to check whether the config loaded successfully. A successful integration logs the config fetch and an installed fetched config line, then fires ready.
Events are not appearing in reports
Check that:
- tracking is not globally disabled with tracking: false
- the specific call is not suppressing tracking with enable_tracking: false
- the goal key is correct
- the visitor context was created successfully
- the process has time to flush events
- short-lived scripts, jobs, or Lambda handlers call CONVERT_SDK.flush
When tracking is disabled, debug logs include messages such as tracking disabled, event suppressed or tracking suppressed for call.
Events are missing from Sidekiq jobs
Add a shutdown flush:
Sidekiq.configure_server do |config|
config.on(:shutdown) { CONVERT_SDK.flush }
end
This drains queued events synchronously before the worker terminates.
AWS Lambda events are missing
Use timer-off mode and flush before returning:
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV["CONVERT_SDK_KEY"],
data_refresh_interval: nil,
flush_interval: nil
)
def handler(event:, context:)
ctx = CONVERT_SDK.create_context(event["visitorId"])
variation = ctx.run_experience("homepage-test")
CONVERT_SDK.flush
{ variation: variation.key }
end
Lambda freezes the environment after return, so background timers are not reliable there.
Need more logging
Enable TRACE logging and pass a logger sink when creating the client:
require "convert_sdk"
require "logger"
CONVERT_SDK = ConvertSdk.create(
sdk_key: ENV.fetch("CONVERT_SDK_KEY"),
log_level: ConvertSdk::LogLevel::TRACE,
sink: Logger.new($stdout)
)
The sink must be passed at create time to observe construction-time logs, including the initial config fetch.
Conclusion
The Convert Ruby SDK provides a fast way to run server-side experiments and feature flags in Ruby applications. Install the convert_sdk gem, create one reusable SDK client at boot, create a context per visitor, run experiences, branch on variation&.key, and track conversions with track_conversion.
For frontend-facing implementations, decide whether Ruby will fully render the variation or whether the Ruby-assigned decision also needs to be passed into JavaScript or an API response. Keeping backend and frontend assignment aligned is essential for consistent visitor experiences and accurate reporting.