Skip to content

Diagnostics

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

Diagnostics & Support

The SDK exposes three complementary debugging mechanisms: structured diagnostic logging, typed errors with .code and .context attributes, and *Diagnostic result objects that describe evaluation decisions without raising. This page covers all three and walks through what to gather when filing a support ticket.

Diagnostic logging

All internal SDK activity emits structured DEBUG-level log records through the Python logging module on the logger named convert_sdk.

Enable diagnostic output by setting that logger to DEBUG:

import logging

logging.getLogger("convert_sdk").setLevel(logging.DEBUG)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(name)s %(levelname)s %(message)s",
)

Or in Django settings.py:

LOGGING = {
    "version": 1,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {
        "convert_sdk": {
            "handlers": ["console"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

The SDK emits through the logger supplied in SDKConfig.logger (defaults to logging.getLogger("convert_sdk")). It never calls logging.basicConfig(), adds handlers, or sets levels — that is the application's responsibility.

Privacy redaction

Diagnostic logs never emit raw visitor ids, SDK keys, secrets, cookies, or raw attribute mappings. Sensitive values are replaced with "<redacted>". Visitor ids are replaced with a hashed 16-character visitor_ref prefix.

Typed errors

All SDK errors derive from ConvertSDKError and carry structured metadata:

class ConvertSDKError(Exception):
    code: str | None         # machine-readable error code
    context: Mapping[str, Any]  # structured metadata (immutable)

Error hierarchy

ConvertSDKError                            (base)
├── ConfigError
│   ├── InvalidConfigError                 (code: "config.invalid")
│   └── ConfigLoadError                    (code: "config_load_failed")
├── TransportError                         (code varies)
└── TrackingDeliveryError                  (code: "tracking_delivery_failed")

# Not top-level exports — import from convert_sdk.errors:
TrackingError                              (programmer-misuse base)
└── ConversionDataError                    (invalid conversion_data value)

Example structured error handling:

from convert_sdk import ConfigLoadError

try:
    core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
except ConfigLoadError as exc:
    print(exc.code)               # "config_load_failed"
    print(dict(exc.context))      # {"endpoint": "...", "status_code": ...}

Goal handling — no GoalNotFoundError

Unknown goals are not exceptions. track_conversion always returns a typed ConversionResult; a missing goal is reflected as ConversionStatus.GOAL_NOT_FOUND:

from convert_sdk import ConversionStatus

result = context.track_conversion("unknown-goal")
if result.status is ConversionStatus.GOAL_NOT_FOUND:
    print(result.reason)     # "goal_not_found"
    print(result.tracked)    # False

To inspect why a goal is missing, use the diagnostic surface:

from convert_sdk import DiagnosticReason

diag = context.diagnose_goal("unknown-goal")
print(diag.reason)   # DiagnosticReason.GOAL_NOT_FOUND
print(diag.message)  # human-readable explanation

Diagnostic result objects

Instead of calling run_experience() (which returns ExperienceResult | None), call diagnose_experience() to get the full decision record without raising:

from convert_sdk import DiagnosticReason

diag = context.diagnose_experience("checkout-experiment")

print(diag.resolved)     # bool — True when bucketed
print(diag.reason)       # DiagnosticReason enum value
print(diag.message)      # human-readable description
print(dict(diag.details))  # redaction-safe operational fields

The same pattern applies to features, goals, and arbitrary config entities:

feat_diag = context.diagnose_feature("checkout-banner")
goal_diag = context.diagnose_goal("purchase_completed")
entity_diag = context.diagnose_entity("experience", "checkout-experiment")

See Type Hints for the field-by-field reference of each diagnostic class.

DiagnosticReason codes

DiagnosticReason Value Meaning
RESOLVED "resolved" The request resolved to a concrete outcome
AUDIENCE_MISMATCH "audience_mismatch" Visitor did not satisfy audience/location rules
EXPERIENCE_NOT_FOUND "experience_not_found" No experience matched the requested key
FEATURE_NOT_IN_SELECTED_VARIATIONS "feature_not_in_selected_variations" Feature declared but visitor's variation carries no change for it
FEATURE_NOT_FOUND "feature_not_found" No feature matched the requested key
GOAL_NOT_FOUND "goal_not_found" No goal matched the requested key
ENTITY_NOT_FOUND "entity_not_found" Config-entity lookup found no match
PROJECT_MAPPING_REQUIRED "project_mapping_required" Loaded config lacks required project mapping

Cross-SDK comparable fields

The ExperienceDiagnostic.details dict includes operational fields like bucket_value when bucketing was attempted. The bucket value is deterministic across all Convert SDKs for the same (visitor_id, experience_id) pair — you can compare the Python SDK's bucket_value to the JavaScript SDK's output to verify parity.

The parity test suite in the python-sdk repository under tests/parity/ exercises this contract with shared test vectors.

To re-derive a bucket value by hand:

from convert_sdk.evaluation.bucketing import get_bucket_value_for_visitor

bucket = get_bucket_value_for_visitor(
    visitor_id="visitor-abc123",
    experience_id="exp-checkout",
)
print("bucket_value:", bucket)  # compare to JS SDK output

The Python SDK uses a pure-Python MurmurHash3 32-bit implementation with seed 9999. The hash input is always f"{experience_id}{visitor_id}" (experience id first). The JavaScript and PHP SDKs use the same algorithm and input order.

Checking initialization health

import os
from convert_sdk import Core, SDKConfig, ConfigLoadError

try:
    core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
except ConfigLoadError as exc:
    print("code:", exc.code)
    print("context:", dict(exc.context))

print("is_ready:", core.is_ready)
snapshot = core.current_config
if snapshot is not None:
    print("experiences:", len(snapshot.experiences_by_key))
    print("features:", len(snapshot.features_by_key))
    print("goals:", len(snapshot.goals_by_key))

If core.is_ready is True but entity counts are zero, the config was loaded but is empty — check whether the correct environment was requested.

Tracking delivery failures

If events are not appearing in the Convert dashboard:

  1. Confirm core.flush() is called before the process exits or the request completes.
  2. Subscribe to LifecycleEvent.API_QUEUE_RELEASED to capture delivery errors (see Code Examples — Queue Control).
  3. Check TrackingDeliveryError.status_code if a delivery exception was raised.
  4. Confirm that sdk_key is present — the transport requires it for the tracking route.

Filing a bug report

Collect the following before opening an issue:

  1. SDK versionpython -c "import convert_sdk; print(convert_sdk.__version__)"
  2. Python versionpython --version
  3. Initialization mode — SDK key or direct config?
  4. Diagnostic result — For evaluation or tracking failures, use the diagnose_* methods and include the full reason, message, and details fields.
  5. Error details — If an exception was raised, include exc.code and dict(exc.context).
  6. Privacy note — Raw visitor ids and SDK keys are automatically redacted in diagnostic logs; do not manually re-add them to your bug report.

Minimum reproduction

When filing a bug, include a minimal standalone script that reproduces the issue using direct config mode (no network dependency):

from convert_sdk import Core, SDKConfig

config = {
    "account_id": "YOUR_ACCOUNT_ID",
    "project": {"id": "YOUR_PROJECT_ID", "name": "Repro"},
    "experiences": [
        # paste the minimal experience definition from your config
    ],
    "features": [],
    "goals": [],
}

core = Core(SDKConfig(data=config)).initialize()
context = core.create_context("repro-visitor-1", visitor_attributes={"tier": "premium"})
diag = context.diagnose_experience("your-experience-key")
print(diag.resolved, diag.reason, dict(diag.details))
core.close()

Using data instead of sdk_key avoids network dependencies and confidential key exposure in bug reports.

What to read next

Clone this wiki locally