Code Examples

Complete Ruby examples for all SDK methods

Complete, copy-pasteable examples for every public method of the Convert Ruby SDK. The public API is snake_case; only the wire payloads are camelCase (translated internally). All examples assume a single CONVERT_SDK client built once at boot (see Initialization).

Build the client

require "convert_sdk"

# Fetch mode.
CONVERT_SDK = ConvertSdk.create(
  sdk_key:        ENV.fetch("CONVERT_SDK_KEY"),
  sdk_key_secret: ENV["CONVERT_SDK_KEY_SECRET"],
  environment:    "prod"
)

# Direct-data mode (no network fetch).
CONVERT_SDK = ConvertSdk.create(data: load_pre_fetched_config)

Subscribe to lifecycle events

Client#on(event, &block) subscribes to a SystemEvents value (or its matching string) and returns self. Deferred one-shot events replay to late subscribers.

CONVERT_SDK.on("ready") do |_payload, _err|
  # decisions are available from here on
end

CONVERT_SDK.on(ConvertSdk::SystemEvents::BUCKETING) do |payload, _err|
  # payload => { visitor_id:, experience_key:, variation_key: }
end

CONVERT_SDK.on(ConvertSdk::SystemEvents::CONVERSION) do |payload, _err|
  # payload => { visitor_id:, goal_key: }
end

Create a visitor context

create_context(visitor_id = nil, attributes = nil) returns a fresh, independent Context (or nil for a blank visitor id). Attributes accept symbol or string keys.

# Visitor id only.
context = CONVERT_SDK.create_context("visitor-123")

# Visitor id + initial attributes.
context = CONVERT_SDK.create_context("visitor-123", { country: "US", plan: "premium" })

Run a single experience

run_experience(key, attributes = nil) returns a BucketedVariation on a hit or a Sentinel on a miss — never raises, never a bare nil.

variation = context.run_experience("homepage-test")
case variation&.key
when nil          then render_default     # sentinel miss (key is nil)
when "treatment"  then render_treatment
else                   render_variation(variation.key)
end

Per-call attributes are merged over the context's attributes (per-call wins):

variation = context.run_experience("homepage-test", { country: "DE" })

Run all experiences

run_experiences(attributes = nil) returns an Array<BucketedVariation> — misses are filtered out, so the array contains only variations the visitor was actually bucketed into.

context.run_experiences.each do |variation|
  activate(variation.experience_key, variation.key)
end

Evaluate a single feature

run_feature(key, attributes = nil) returns a frozen BucketedFeature (or an Array when several bucketed variations carry the feature). A miss is a DISABLED BucketedFeature, never a sentinel — branch on #status.

feature = context.run_feature("new-checkout")
if feature.status == ConvertSdk::FeatureStatus::ENABLED
  render_new_checkout(feature.variables["headline"])
else
  render_legacy_checkout
end

# narrow which experiences may carry the feature, and read raw stored values
feature = context.run_feature("new-checkout", {
  experience_keys: ["checkout-test"],
  type_casting: false
})

experience_keys limits which experiences the call may bucket at all — and therefore which sticky
variation assignments the read commits: an unnarrowed feature read commits one for every configured
experience. Matching is exact, so a key that merely prefixes a configured key does not match. An
empty array means no filter, not no experiences; an unknown key is skipped rather than raised, and a
filter that matches nothing decides nothing (every declared feature comes back DISABLED). Elements
may be symbols. A value that is not an Array — a bare String, a Hash, an Integer — is
ignored with a warn naming experience_keys, and the call proceeds unfiltered; an explicit nil
is absence and warns nothing.

type_casting: false returns each variable as the config stores it. Only the literal false
disables casting — nil, "false", 0 and true all leave it on. It changes the variable map and
nothing else: the same experiences are decided, the same variation resolved and the same sticky
assignment committed either way, and a variable with no declared type still logs the
variable type not found warn.

Evaluate all features

run_features(attributes = nil) returns an Array<BucketedFeature> — the full roster, every declared feature evaluated for this visitor (enabled + disabled).

context.run_features.each do |feature|
  toggle(feature.key, on: feature.status == ConvertSdk::FeatureStatus::ENABLED)
end

# the same two controls apply
context.run_features({ experience_keys: ["checkout-test"], type_casting: false })

The reserved per-call keys

Every decision entry point takes the same per-call hash. Whatever you put in it is merged over the
context's attributes (per-call wins) and becomes the visitor properties audience rules match
against. Six keys are additionally read as controls; ConvertSdk::Context::RESERVED_KEYS
enumerates them, and the translation into the decision engine is built from that enumeration rather
than from a hand-written list.

Reserved keyWhere it appliesWhat it does
location_propertiesevery decision entry pointsupplies the location-matching properties (location matching never falls back to the visitor properties)
environmentevery decision entry pointsupplies the environment the environment-match step reads
enable_trackinghonoured on run_experience / run_experiences; accepted and inert on run_feature / run_featuresfalse skips the outbound event enqueue
experience_keysrun_feature / run_features onlynarrows which experiences the read decides
type_castingrun_feature / run_features onlyfalse returns variables as the config stores them
ruleDatarun_custom_segments onlymerged over the visitor properties the segment rules match — the one camelCase key on a snake_case surface

All six accept a symbol or a string key. They differ in where they are read from, and in one case
that is observable: location_properties and environment are read from the merged map, so
setting either on create_context reaches the engine even with no per-call hash at all. The other
four are read from the raw per-call hash only — so an enable_tracking: false passed as a
create_context attribute is inert and tracking stays on; only a per-call one suppresses.
run_custom_segments reads ruleData and nothing else out of its hash.

Two keys that look like controls are not. enable_storage and update_visitor_properties are
engine-readable names the per-call hash deliberately never lifts: passing enable_storage: false
does not stop the sticky StoreData write (Context#set_preview is the only thing that gates it),
and an update_visitor_properties entry is not a call to the method of that name. Both are merged
into the visitor properties like any other attribute, where they match no control — and where an
audience rule could legitimately match on them. features is not reserved either: a per-call
features key is an ordinary visitor property, and run_features keeps its DISABLED padding.

Track a conversion

track_conversion(goal_key, goal_data: nil, force_multiple_transactions: false) records a conversion, deduplicated per visitor per goal. Returns self.

# Bare conversion.
context.track_conversion("signup")

# With revenue / transaction data (snake_case goal-data keys).
context.track_conversion(
  "purchase",
  goal_data: { amount: 49.99, products_count: 3, transaction_id: "tx-42" }
)

The eight accepted goal_data keys are listed in Return Types & Sentinels. Unknown keys are rejected and debug-logged.

Force a repeat transaction (bypass dedup)

By default a goal records once per visitor. For renewals or repeat purchases, bypass the dedup check:

context.track_conversion(
  "purchase",
  goal_data: { amount: 49.99, transaction_id: "tx-43" },
  force_multiple_transactions: true
)

force_multiple_transactions: true enqueues the repeat transaction without re-marking the goal.

Suppress tracking for a single call

Pass enable_tracking: false in the per-call attributes hash to evaluate but not report (e.g. a consent-denied flow). Bucketing, sticky persistence, and the bucketing lifecycle event still fire — only the outbound network enqueue is skipped.

variation = context.run_experience("homepage-test", { enable_tracking: false })
context.run_experiences({ enable_tracking: false })

This applies to the experience calls. run_feature and run_features enqueue no bucketing event at
all, so they accept enable_tracking and it changes nothing. The inertness runs both ways:
run_experience and run_experiences accept experience_keys and type_casting and decide
exactly as if they were absent.

The global tracking: false config switch always wins over a per-call true. See Tracking Control.

Force a variation for a preview link

set_preview(experience_id:, variation_id:) forces one variation of one experience on this context, bypassing audiences, locations, environment, experience and variation status, traffic filters, stored decisions, and bucketing for that experience. Returns self. It is synchronous — when the previewed experience is not in the installed config (a draft or paused target), the SDK fetches just that experience on demand.

ConvertSdk.parse_preview_param(value) parses a preview link's {experienceId}.{variationId} value. It is pure and never raises: a two-element Array of strings on success, nil for a missing, malformed, or non-numeric value — so the guard falls straight through to normal decisioning.

# `raw` is whatever your app read from the preview link / request / cookie.
if (pair = ConvertSdk.parse_preview_param(raw))
  context.set_preview(experience_id: pair[0], variation_id: pair[1])
end

# The previewed experience now returns the forced variation.
variation = context.run_experience("homepage-test")

An unresolvable experience or an unknown variation id clears preview and logs a warn — the next run_experience decides exactly as if set_preview had never been called. A blank experience_id/variation_id is rejected the same way but leaves an earlier successful preview in place. Nothing raises. Preview state lives on this one Context instance; two contexts never share it.

A preview-active context leaves zero trace: no tracking events are enqueued, no bucketing or conversion lifecycle event fires, track_conversion is a full no-op, and no visitor state is written — for every experience on that context, not just the previewed one. Other experiences still decide and render normally. See Tracking Control and QA & Preview.

Set default report-segments

set_default_segments(segments) attaches default report-segments for the visitor (filtered to the platform report keys), merged into the visitor's stored segments. Returns self. Supply the wire keys (visitorType, customSegments, …).

context.set_default_segments({ "visitorType" => "new", "customSegments" => ["beta"] })

Run custom segments

run_custom_segments(segment_keys, attributes = nil) evaluates named custom segments against the visitor's properties and attaches matching ids under customSegments in StoreData. Returns a propagated RuleError sentinel, or nil.

context.run_custom_segments(["high-value", "returning"])

# With per-call rule data.
context.run_custom_segments(["high-value"], { ruleData: { lifetime_value: 1250 } })

See Segments.

Update visitor properties (sticky)

update_visitor_properties(properties) merges sticky properties into both the in-memory attributes (so a later decision on this context sees them immediately) and the store (so a later context for the same visitor sees them — stickiness). Returns self.

context.update_visitor_properties({ plan: "premium", account_age_days: 120 })

See Visitor Context.

Read persisted visitor data

get_visitor_data returns the visitor's stored, string-keyed StoreData, or the empty shape { "bucketing" => {}, "segments" => {}, "goals" => {} } when nothing is stored.

data = context.get_visitor_data
data["bucketing"] # sticky variation assignments
data["segments"]  # attached report-segments
data["goals"]     # converted goals (dedup state)

Look up a config entity

get_config_entity(key, entity_type) looks up a config entity by key. entity_type is one of :experience, :feature, :goal (symbol or string). Returns the frozen entity hash, or nil on a miss (debug-logged).

experience = context.get_config_entity("homepage-test", :experience)
feature    = context.get_config_entity("new-checkout", :feature)
goal       = context.get_config_entity("purchase", :goal)

Flush queued events

flush(reason = nil) (alias release_queues) delivers queued events synchronously and returns self. An empty queue is a no-op. Long-running servers also drain via the background flush timer; short-lived processes must flush explicitly before exit.

CONVERT_SDK.flush
CONVERT_SDK.flush("shutdown")   # optional human-readable reason (logged)
CONVERT_SDK.release_queues       # the frozen-name alias — same path

Re-arm after a fork

postfork explicitly re-arms the client after a fork — rarely needed (automatic Process._fork detection covers the common runtimes). Returns self. See Fork Safety & Runtime Recipes.

# e.g. after Process.daemon, or in a worker-boot hook for belt-and-braces
CONVERT_SDK.postfork

Next steps


Did this page help you?