Skip to content

MigrationFromJavascript

Usman Abbas edited this page Jun 8, 2026 · 6 revisions

Migrating from the JavaScript SDK

This guide is for teams that are familiar with the Convert JavaScript SDK and want to adopt the Python SDK — either porting backend Node.js code to Python or sharing mental models with a JavaScript front end.

The two SDKs are behaviorally equivalent: the same (visitor_id, experience_id) pair produces the same variation in both. The API surface uses Pythonic conventions rather than JavaScript idioms.

Concept map

JavaScript SDK Python SDK Notes
Core constructor Core(SDKConfig(...)).initialize() Python uses a dataclass config; initialize() is explicit
core.onReady() core.is_ready Python init is synchronous; no async promise
core.createContext(visitorId, attributes) core.create_context(visitor_id, visitor_attributes=attributes) snake_case; visitor_attributes= kwarg
context.runExperience(key, opts) context.run_experience(key, attributes=...) Returns typed frozen dataclass, not plain dict
context.runExperiences(opts) context.run_experiences() Returns list[ExperienceResult]
context.runFeature(key, opts) context.run_feature(key) Returns FeatureResult | None
context.runFeatures(opts) context.run_features() Returns list[FeatureResult]
context.trackConversion(key, data) context.track_conversion(key, revenue=..., conversion_data=...) Returns ConversionResult; unknown goal is NOT an exception
context.setDefaultSegments(segments) context.set_segments(segments) Takes a dict of segment keys; kept separate from set_attributes
context.runCustomSegments(keys, attrs) context.run_custom_segments(keys, rule_data=...) Returns CustomSegmentsResult
core.on(event, handler) core.on(LifecycleEvent.X, handler) Typed enum instead of string; handler signature: (payload, error=None)
context.releaseQueues() core.flush() Flush is on Core, not Context; synchronous
dataRefreshInterval: 300000 (default-on, ms) SDKConfig(refresh=RefreshConfig(interval_seconds=300.0)) (opt-in, seconds) Unit and default differ — see "Background config refresh" below
'config.updated' event LifecycleEvent.CONFIG_UPDATED JS fires after every successful fetch with non-empty data; Python fires only when the snapshot actually differs

Initialization

JavaScript:

import { Core } from '@convertcom/js-sdk';

const core = new Core({ sdkKey: process.env.CONVERT_SDK_KEY });
await core.onReady();

Python:

import os
from convert_sdk import Core, SDKConfig

core = Core(
    SDKConfig(
        sdk_key=os.environ["CONVERT_SDK_KEY"],
        environment="production",
    )
).initialize()
# core.is_ready is True immediately — init is synchronous
assert core.is_ready

Python's Core.initialize() is synchronous and blocking. There is no on_ready() coroutine because initialization does not use an async event loop. If the config fetch fails, ConfigLoadError is raised at initialize() time, not deferred.

Context creation

JavaScript:

const context = core.createContext('visitor-abc123', {
  browser: 'chrome',
  country: 'US',
});

Python:

context = core.create_context(
    "visitor-abc123",
    visitor_attributes={"browser": "chrome", "country": "US"},
)

The Python Context object is reusable across multiple evaluations. Update stored attributes with context.set_attributes(...) and default segments with context.set_segments(...) (kept strictly separate).

Running experiences

JavaScript:

const result = context.runExperience('checkout-flow', {
  locationAttributes: { path: '/checkout' },
});

if (result) {
  console.log(result.variationKey);
}

Python:

result = context.run_experience("checkout-flow")

if result is not None:
    print(result.variation_key, result.variation_id)

The Python result is a frozen dataclass (ExperienceResult) rather than a plain dict. All fields are typed — see Type Hints.

Running features

JavaScript:

const feature = context.runFeature('checkout-banner', {
  locationAttributes: { path: '/checkout' },
});

if (feature) {
  console.log(feature.status, feature.variables);
}

Python:

from convert_sdk import FeatureStatus

feature = context.run_feature("checkout-banner")

if feature is not None:
    print(feature.status.value)       # "enabled" or "disabled"
    print(dict(feature.variables))    # type-cast variable dict

FeatureStatus is an enum. Compare against the enum members (feature.status == FeatureStatus.ENABLED) for type safety.

Tracking conversions

JavaScript:

context.trackConversion('purchase', {
  goalData: [
    { key: 'revenue', value: 49.99 },
    { key: 'products_count', value: 2 },
  ],
});

Python:

from convert_sdk import ConversionStatus

result = context.track_conversion(
    "purchase_completed",
    revenue=49.99,
    conversion_data={"products_count": 2},
)
# result is always a typed ConversionResult — never raises for missing goal
if result.status is ConversionStatus.GOAL_NOT_FOUND:
    print(result.reason)   # "goal_not_found"

Key differences:

  • revenue is a named kwarg (not inside conversion_data)
  • conversion_data is a flat Mapping[str, Any]
  • An unknown goal returns ConversionResult(status=GOAL_NOT_FOUND) — it does not raise an exception. Use result.status or context.diagnose_goal(key) to handle missing goals.

Segments

JavaScript:

context.setDefaultSegments({ browser: 'CH', country: 'US' });
const matched = context.runCustomSegments(['segment-key-1'], {});

Python:

context.set_segments({"loyalty_tier": "gold"})
result = context.run_custom_segments(
    ["segment-premium-eu", "segment-mobile"],
    rule_data={"device": "mobile"},
)
# result: CustomSegmentsResult
# result.matched_segment_ids: tuple[str, ...] — only newly matched IDs

Note: set_segments and set_attributes are distinct methods for distinct state concerns — segments and attributes are kept strictly separate.

Lifecycle events

JavaScript:

core.on('conversionCreated', (payload) => {
  console.log(payload.goalId);
});

Python:

from convert_sdk import LifecycleEvent
from convert_sdk.events import ConversionEventPayload

def on_conversion(payload: ConversionEventPayload, error=None) -> None:
    print(payload.goal_key, payload.visitor_id)

core.on(LifecycleEvent.CONVERSION, on_conversion)

Python uses a typed LifecycleEvent enum (not bare strings). Each event type carries a distinct typed payload dataclass — there is no single generic payload type. Handlers always receive (payload, error=None).

Queue release

JavaScript:

await context.releaseQueues();

Python:

core.flush()   # synchronous; no return value

Flush is on Core, not Context. Python's flush() is synchronous. There is no async variant in the default transport. See Extending for how to replace the transport.

Background config refresh

The two SDKs handle background config refresh differently. A direct port without reading this section will run on stale config indefinitely.

Concern JavaScript Python
Default behaviour Always on Off (SDKConfig.refresh = None)
Config field dataRefreshInterval SDKConfig.refresh = RefreshConfig(...)
Units milliseconds seconds
On transient error Logs and stops rescheduling Exponential backoff, keeps retrying
On bad upstream payload Logs the parse failure Stops worker, logs terminal failure
Update event 'config.updated' fires after every successful fetch CONFIG_UPDATED fires only when the snapshot actually differs
Observability None on the public surface core.current_config

JavaScript:

const core = new Core({
  sdkKey: process.env.CONVERT_SDK_KEY,
  dataRefreshInterval: 300000, // 5 minutes, in milliseconds
});

core.on('config.updated', () => {
  myCache.invalidate();
});

Python:

import os
from convert_sdk import Core, SDKConfig, LifecycleEvent, RefreshConfig

core = Core(
    SDKConfig(
        sdk_key=os.environ["CONVERT_SDK_KEY"],
        refresh=RefreshConfig(interval_seconds=300.0),  # 5 minutes, in SECONDS
    )
).initialize()

core.on(LifecycleEvent.CONFIG_UPDATED, lambda payload, error=None: my_cache.invalidate())

The Python SDK also exposes a richer policy surface (jitter_seconds, backoff_factor, backoff_max_seconds) that the JavaScript SDK does not. See Initialization § automatic config refresh.

Deliberate Pythonic differences

Area JavaScript Python Why
Naming camelCase snake_case PEP 8 convention
Results plain object frozen dataclass Immutability, type safety
Async Promise / await synchronous (blocking) Python SDK is sync-first
Config object {sdkKey, environment, ...} SDKConfig(sdk_key=..., environment=...) Typed frozen dataclass
Errors thrown Error objects typed exceptions with .code and .context Structured error handling
Goal miss throws exception ConversionResult(status=GOAL_NOT_FOUND) Normal misses are results, not exceptions
Event payloads single generic payload distinct frozen dataclass per event Type safety
Diagnostics console-level debug logging integration Python stdlib conventions
Extension subclassing / plugins Protocol implementations Structural typing

Behavioral equivalence

The bucketing algorithm is identical between the two SDKs. For the same (visitor_id, experience_id) pair, both SDKs compute the same bucket value and select the same variation. This is verified by the parity test suite at tests/parity/ in the python-sdk repository.

The hash input format is f"{experience_id}{visitor_id}" (experience id concatenated before visitor id) with MurmurHash3 32-bit seed 9999. If you compute the bucket value manually in JavaScript and compare it to the Python SDK's result, the values will match.

Future async / framework support

The MVP is sync-first. An async public API (AsyncCore / AsyncContext) and framework-specific helpers (convert-sdk-django, convert-sdk-fastapi, convert-sdk-flask) are planned for Phase 3 and will share the same evaluation core, parity contracts, and adapter Protocols as the sync surface. See Async and Framework Integrations for the design intent.

What to read next

  • Code Examples — full run_experience() / run_feature() reference
  • Diagnosticsdiagnose_experience() replaces JS SDK debug mode
  • Extending — replacing transport/storage adapters

Clone this wiki locally