Skip to content

Initialization

Ahmed Abbas edited this page Jun 8, 2026 · 1 revision

Initialization

ConvertSdk.create is THE public entry point. It builds the validated configuration, wires the SDK's managers, drives the config lifecycle, and returns a ready-to-use ConvertSdk::Client. Build exactly one client per process and reuse it for the process lifetime — the client owns the shared config, store, event, and delivery surfaces.

create is the SDK's only raising surface: it raises ArgumentError on misconfiguration (missing sdk_key/data, bad types, an unknown option key). Every other public method degrades to a documented return value and a log line instead of raising.

The factory

ConvertSdk.create(sdk_key: nil, data: nil, store: nil, clock: nil, sink: nil, **options) #=> Client

The keyword arguments fall into two groups: the wiring seams (store:, clock:, sink:) extracted before config validation, and **options — any config option (sdk_key_secret:, environment:, log_level:, timeouts, …).

Fetch mode vs direct-data mode

The client supports two mutually-exclusive config sources. At least one of sdk_key: / data: is required (otherwise ArgumentError).

Fetch mode — sdk_key:

Pass an sdk_key: and the client fetches config synchronously at construction via GET {config_endpoint}/config/{sdk_key} (with ?environment=... appended when environment: is set). When sdk_key_secret: is configured, an Authorization: Bearer {secret} header is attached (the secret is redacted from every log line).

CONVERT_SDK = ConvertSdk.create(
  sdk_key:        ENV.fetch("CONVERT_SDK_KEY"),
  sdk_key_secret: ENV["CONVERT_SDK_KEY_SECRET"] # optional bearer secret
)

A failed fetch never raises — it is logged at warn and the client is constructed without config (degrade-gracefully). It may fall back to a non-stale cached entry from the store (meaningful across processes with a shared RedisStore).

Direct-data mode — data:

Pass a pre-fetched config object and no network fetch happens — the inline object is normalised to string keys and installed straight away. This is the single network-free path, ideal for tests and advanced setups:

config = load_pre_fetched_config # your own loader / fixture
CONVERT_SDK = ConvertSdk.create(data: config)

Symbol-keyed inputs are accepted — they are deep-stringified at the public boundary so the installed snapshot matches the string-keyed wire shape exactly.

The ready event

The first successful config install (fetched or direct) fires ready exactly once for the client's lifetime. Subsequent installs (the background refresh) fire config.updated, never ready again.

Because fetch mode installs config synchronously inside create, a client returned from create in fetch mode is typically already ready. To observe readiness explicitly — and to catch late subscribers — subscribe with Client#on; the SDK replays deferred one-shot events to subscribers that register after the event fired:

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

Client#on accepts a SystemEvents value or its matching string: ready (SystemEvents::READY), config.updated (CONFIG_UPDATED), bucketing (BUCKETING), and conversion (CONVERSION). It returns self, so it chains. See Event System.

Optional seams: sink:, store:, clock:

These three keyword arguments are extracted before config validation — they are not config options.

  • sink: — an initial log sink (anything responding to debug/info/warn/error, e.g. the stdlib Logger). Passing it at create time — rather than attaching one afterward — makes the full lifecycle observable, including the construction-time config fetch. Without this seam you could only attach a sink after create and would miss every init-time line.

    require "logger"
    CONVERT_SDK = ConvertSdk.create(
      sdk_key:   ENV.fetch("CONVERT_SDK_KEY"),
      log_level: ConvertSdk::LogLevel::TRACE,
      sink:      Logger.new($stdout)
    )
  • store: — an optional data store for sticky bucketing and goal deduplication. Defaults to an in-process MemoryStore. Pass a ConvertSdk::Stores::RedisStore (or any object that duck-types get/set) for cross-process state. See Configuration Options and Persistent DataStore.

  • clock: — an optional monotonic time source (#call → seconds) for the config-cache TTL math. Defaults to the SDK's monotonic clock. Injectable so tests control staleness without real waits.

No threads until first use

create starts no background threads (NFR4 lazy start). The background config-refresh timer is wired at construction but only started on the first create_context call. This is core to the SDK's zero-config fork safety — a client built in a preloading master carries no thread state across a fork. See Fork Safety & Runtime Recipes.

Creating a visitor context

A Context binds one visitor to the client. create_context returns a new, independent context — call it once per request/job:

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

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

A blank or nil visitor id logs an error and returns nil (never an ArgumentError — per-request validation respects the never-crash contract). Attributes are deep-stringified at the boundary, so symbol-keyed Ruby hashes and string-keyed Rails params behave identically. See Visitor Context.

Next steps

Clone this wiki locally