Convert Ruby SDK
The official Convert Experiences FullStack Ruby SDK — server-side A/B testing, feature flags, and personalizations for Ruby applications. Bucketing-compatible with the Convert JavaScript SDK. Zero runtime dependencies (stdlib only).
- Fork-safe with zero configuration — works under Puma cluster, Unicorn, Passenger, Sidekiq, AWS Lambda, and plain CLI scripts. See Fork safety below — it's the differentiator.
- Never crashes the host — every public method degrades to a documented
return value and a log line; only misconfiguration at
ConvertSdk.createraises (anArgumentError). - Ruby ≥ 3.1 — CRuby 3.1–3.4 and JRuby are supported.
Install
# Gemfile
gem "convert_sdk"
gem install convert_sdk
5-minute start
The complete flow — build a client, create a per-visitor context, decide an experience, act on the result, track a conversion, and flush — in one copy-pasteable block:
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 a pre-fetched `data:`.)
CONVERT_SDK = ConvertSdk.create(sdk_key: ENV.fetch("CONVERT_SDK_KEY"))
# 2. One context per visitor (per web request / per job). Cheap — no network,
# no thread.
context = CONVERT_SDK.create_context("visitor-123", { "country" => "US" })
# 3. Decide an experience. Returns a BucketedVariation on a hit, or a Sentinel
# on a miss — NEVER raises, NEVER a bare nil.
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 (a Sentinel's #key is always nil), so a single `case` covers both.
case variation&.key
when nil then render_default # business miss — show the control
when "treatment" then render_treatment
else render_variation(variation.key)
end
# 5. Track a conversion with revenue. Deduplicated per visitor per goal.
context.track_conversion("purchase", goal_data: { amount: 49.99, transaction_id: "tx-1" })
# 6. Flush queued events synchronously. In long-running servers the background
# timer also drains; call flush explicitly before a process exits (Lambda/CLI).
CONVERT_SDK.flush
Production wiring per runtime — Rails, Sidekiq, AWS Lambda, and CLI recipes live in the wiki: Fork Safety & Runtime Recipes and Quickstart.
Fork safety (zero config)
Fork safety is the SDK's flagship guarantee, so it leads the docs.
The claim: build the client once, let your server fork workers, and events
are delivered from every forked worker — with zero fork-handling code in your
app. No postfork, no on_worker_boot hook required.
How it works:
- At
require "convert_sdk"the SDK installs a singleProcess._forkhook (its only global mutation). The hook is cheap and starts no threads. - The SDK starts no background threads until first use — a client built in a
preloading master (Puma
preload_app!) carries no thread state across the fork. - On the first decision in a forked worker, the
_forkdetection plus PID-guarded flush boundaries automatically re-arm the client (timers re-start lazily, the queue's process ownership resets) — so the worker decides and delivers on its own.
When you need postfork: only for setups that bypass Process._fork
entirely (or daemonize via Process.daemon), or if you prefer an explicit
re-arm (LaunchDarkly-style). The
fork/daemon matrix in the troubleshooting guide
spells out exactly which runtimes are automatic and which need an explicit
CONVERT_SDK.postfork call. The four quickstarts ship the right wiring for each.
Public API
The full public API is documented in the wiki and the YARD API reference. The
entry point is ConvertSdk.create (factory); it returns a Client with
#create_context, #flush, #postfork, and #on. Each Context exposes
#run_experience, #run_feature, #track_conversion, and related methods.
See Code Examples and
the API reference (YARD) for full
signatures.
The sentinel return contract
Decisioning methods never raise and never return a bare nil for a
business miss. They return a value object:
- A hit returns a frozen
BucketedVariation(orBucketedFeature):#keyis the real key,#error?isfalse. - A miss returns a frozen
Sentinel:#keyis alwaysnil,#error?is alwaystrue, and#to_sis the wire string.
This is why the documented branch pattern works for both cases at once:
case (variation = context.run_experience("homepage-test")).key
when nil then render_default # Sentinel — a business miss
else render_variation(variation.key)
end
For features, branch on #status instead (a feature miss is a DISABLED
BucketedFeature, never a sentinel):
feature = context.run_feature("new-checkout")
if feature.status == ConvertSdk::FeatureStatus::ENABLED
render_new_checkout(feature.variables["headline"])
else
render_legacy_checkout
end
Configuration
All configuration options are passed as keyword arguments to ConvertSdk.create.
See the Configuration wiki page
for the full option table with defaults. Pass data_refresh_interval: nil and
flush_interval: nil for Lambda/CLI (timer-off mode).
Data stores
Sticky bucketing and goal deduplication persist through a store port (default:
in-process MemoryStore). See
Configuration for
the RedisStore recipe and custom store duck-typing contract.
Documentation
Full developer documentation lives in the Convert Ruby SDK wiki:
- Quickstart · Installation · Initialization
- Configuration · Return Types & Sentinels · Code Examples
- Fork Safety & Runtime Recipes · Tracking Control · Testing
- Core concepts & how-to: bucketing algorithm, rule evaluation, running experiences (see the wiki sidebar)
- API reference (YARD) — generated method-level docs
- Contributing
Development
bundle install # install dev/test dependencies
bundle exec rake # the default task: RSpec + RuboCop
bundle exec rbs -r net-http -r uri -r json -I sig validate # validate RBS signatures
bundle exec steep check # static type check
Publishing is handled exclusively by the OIDC release workflow — there is no
rake release task. See CONTRIBUTING.md for the release
process.
License
Apache-2.0. See LICENSE.