-
Notifications
You must be signed in to change notification settings - Fork 0
Initialization
Core is the SDK entry point. You construct it with an SDKConfig, then call
initialize(). An SDKConfig must carry exactly one config source:
-
data— a preloaded config dict. Initialization makes no network call. Ideal for unit tests, CI, and any environment that loads config out of band. -
sdk_key— the SDK fetches config over HTTPS through the built-inhttpx-backed transport.initialize()is synchronous and blocks until the fetch completes or raises.
Both forms return the same Core instance from initialize(), at which point
core.is_ready is True.
Public symbols used on this page are importable from convert_sdk:
Core, SDKConfig, TransportConfig, RefreshConfig, LifecycleEvent,
InvalidConfigError, ConfigLoadError, TransportError.
Pass a config dict as data=. No transport is constructed or used:
from convert_sdk import Core, SDKConfig
project_config = {
"account_id": "1001",
"project": {"id": "2002", "name": "Demo"},
"experiences": [],
"features": [],
"goals": [],
}
core = Core(SDKConfig(data=project_config)).initialize()
assert core.is_readyThis form is safe in unit tests, offline tooling, and environments where config
is managed outside the SDK. No httpx client is created.
Read the key from the environment — never hard-code credentials:
import os
from convert_sdk import Core, SDKConfig
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
assert core.is_readyinitialize() fetches config over HTTPS from https://cdn-4.convertexperiments.com
and blocks until the response arrives. Network failures raise ConfigLoadError;
malformed config raises InvalidConfigError. A non-HTTPS base_url raises
TransportError at TransportConfig construction, before any network I/O
(NFR8: TLS-only transport).
Customise the transport endpoint, timeout, or TLS behavior via
TransportConfig on SDKConfig.transport:
import os
from convert_sdk import Core, SDKConfig, TransportConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
environment="staging",
transport=TransportConfig(
base_url="https://cdn-4.convertexperiments.com",
timeout=5.0,
auth_secret=os.environ.get("CONVERT_AUTH_SECRET"),
),
)
).initialize()You can also request a low-cache config route, which matches the JS SDK
_conv_low_cache=1 query parameter:
import os
from convert_sdk import Core, SDKConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
cache_level="low",
)
).initialize()Core implements the context-manager protocol. Prefer it in long-lived services
and scripts so transport resources are always released — even if an exception is
raised inside the block:
from convert_sdk import Core, SDKConfig
with Core(SDKConfig(data=project_config)).initialize() as core:
context = core.create_context("visitor-001")
result = context.run_experience("checkout-experiment")
# ... evaluate / track ...
# core.close() is called automatically on exit, stopping background threads
# and closing the httpx transport if Core created it.Using the context-manager form is equivalent to calling core.close() in a
finally block.
Once initialized, call core.create_context(visitor_id) to get a
per-visitor Context. Pass visitor_attributes for audience qualification:
context = core.create_context(
"visitor-abc",
visitor_attributes={"country": "DE", "plan": "pro"},
)Attributes are copied defensively — later mutations to the dict you pass never
affect the context. The context is caller-scoped; Core does not cache it.
Create one per request (or per visitor lifetime) and reuse it within that scope.
See Visitor Context for the full per-visitor API.
Core is designed as a long-lived singleton. Create one instance at application
startup and share it across requests. The tracking queue and snapshot are
thread-safe:
# Application startup (module level or app factory):
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
# Per-request handler:
def handle_request(visitor_id: str) -> None:
context = core.create_context(visitor_id)
result = context.run_experience("checkout-flow")
...Errors raised during initialization are all subclasses of ConvertSDKError:
| Error class | When raised |
|---|---|
InvalidConfigError |
Neither sdk_key nor data provided; both provided; sdk_key is empty; data is not a dict; cache_level invalid; batch_size / auto_flush_interval_ms invalid |
ConfigLoadError |
Network or HTTP error while fetching config with sdk_key
|
TransportError |
Non-HTTPS base_url, or transport construction failure |
import os
from convert_sdk import Core, SDKConfig, ConfigLoadError, InvalidConfigError
try:
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
except ConfigLoadError as exc:
# Network error or non-2xx response fetching the config.
print("config fetch failed:", exc)
except InvalidConfigError as exc:
# Config payload is malformed (missing required fields, wrong types, etc.)
print("invalid config:", exc)After initialize() succeeds, normal evaluation and tracking outcomes are
never raised — they are returned as typed result objects (e.g.
ExperienceResult | None, ConversionResult). Exceptions signal programmer
errors or delivery failures only.
Post-MVP (Phase 2, FR31). Off by default. Omitting
refresh=(or passingrefresh=None) preserves MVP behavior: no daemon thread, no refresh events, no added cost.
Long-running services can opt into periodic background config refresh by
supplying a RefreshConfig on SDKConfig.refresh. The SDK starts one daemon
worker thread that re-fetches config on a configurable interval and
atomically swaps the immutable snapshot. In-flight evaluations always see
a single coherent snapshot — the swap is mutex-guarded (see
ADR 0001).
import os
from convert_sdk import Core, SDKConfig, RefreshConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
refresh=RefreshConfig(
interval_seconds=300.0, # base poll period (default: 5 minutes)
jitter_seconds=30.0, # +U(0, jitter) per cycle — avoids thundering herd
backoff_factor=2.0, # exponential backoff multiplier on failures
backoff_max_seconds=600.0, # backoff ceiling (never tight-loops on failure)
),
)
).initialize()
# Optionally trigger an immediate out-of-band refresh:
core.refresh_now() # fire-and-forget; returns before the fetch completes
# Graceful shutdown stops the worker thread:
core.close()refresh_now() is a safe no-op when refresh is disabled (refresh=None),
when the SDK is in direct-config (data) mode, or before initialize().
On every successful snapshot swap the SDK emits LifecycleEvent.CONFIG_UPDATED
on the event bus, carrying the new account_id, project_id, and per-type
entity_counts. Subscribe to bust any caches derived from config:
from convert_sdk import LifecycleEvent
core.on(
LifecycleEvent.CONFIG_UPDATED,
lambda payload, error=None: my_cache.clear(),
)Handlers registered with core.on(event, handler) are safe to call before
initialize(). A handler that raises is isolated and logged; it never disrupts
evaluation or delivery.
A Context retains the snapshot that was current when it was created. This
gives each request a coherent view for its full duration even if a refresh fires
mid-request. Create a fresh context (per request or per visitor interaction) to
pick up the latest config.
- One refresh worker per
Coreinstance, running as a daemon thread. - Refresh is process-local — no cross-process coordination.
- Background failures never crash the host process. The prior good snapshot keeps serving and the failure is logged through the diagnostic logger.
-
Do not rely on refresh surviving a
fork()withoutexec(). The daemon thread is not duplicated into the child. In pre-fork servers (Gunicorn, Celery prefork) re-initialize the SDK in each worker process after fork. - Supplying a
RefreshConfigin direct-config (data) mode starts no worker — there is no remote endpoint to poll — and the SDK logs arefresh.skippeddiagnostic.
A misconfigured RefreshConfig raises InvalidConfigError at construction:
| Rule | Error condition |
|---|---|
interval_seconds > 0 |
Zero or negative value |
0 <= jitter_seconds <= interval_seconds |
Negative, or greater than interval |
backoff_factor >= 1.0 |
Less than 1.0 |
backoff_max_seconds >= interval_seconds |
Less than interval |
See the full field reference in Configuration.
-
Configuration — field-by-field reference for
SDKConfig,TransportConfig,RefreshConfig -
Code Examples —
run_experience,run_feature,track_conversion,core.flush, lifecycle events -
Visitor Context —
create_context,set_attributes,set_segments - Extending — custom transport, custom data store, event-bus observers
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Getting Started
Python SDK
- Quickstart
- Installation
- Initialization
- Configuration
- Code Examples
- Type Hints
- Diagnostics
- Extending
- Testing
- Async & Frameworks
Migration
Core Concepts
- Experiences & Variations
- Feature Flags
- Bucketing Algorithm
- Rule Evaluation
- Segments
- Data Management
- Event System
- API Communication
How-To Guides
- Running Experiences
- Running Features
- Tracking Conversions
- Visitor Context
- Persistent DataStore
- Troubleshooting
Edge & Integrations
Maintainers