Skip to content

Diagnostics

Usman Abbas edited this page May 5, 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.diagnostics.

Enable diagnostic output by setting that logger to DEBUG:

import logging

logging.getLogger("convert_sdk.diagnostics").setLevel(logging.DEBUG)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(name)s %(message)s %(extra)s",  # adjust to your formatter
)

Or in Django settings.py:

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

Loggers used by the SDK:

Logger name Content
convert_sdk.diagnostics Evaluation, bucketing, and tracking lifecycle events
convert_sdk.tracking Delivery warnings when HTTP transport fails
convert_sdk.refresh Background config-refresh callback errors

Diagnostic log record structure

Each record has two extra fields:

Field Description
sdk_event Dot-separated event name, e.g. evaluation.experience.completed
sdk_details Redacted dict of event-specific fields

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 16-character SHA-256 prefix (visitor_ref).

Key diagnostic events

sdk_event Fired by Useful fields
sdk.initialization.started Core.__init__ source, transport_type
sdk.initialization.succeeded Core.__init__ is_ready, entity_counts
sdk.initialization.failed Core.__init__ error_type, error_code
context.created Core.create_context had_existing_state, supplied_visitor_attribute_count
evaluation.experience.completed Context.run_experience / diagnose_experience matched, reason, variation_key, bucket_value
evaluation.experiences.completed Context.run_experiences result_count
evaluation.feature.completed Context.run_feature / diagnose_feature matched, reason, status
evaluation.features.completed Context.run_features result_count
evaluation.custom_segments.completed Context.run_custom_segments matched_segment_count
tracking.conversion.started Context.track_conversion has_conversion_data
tracking.conversion.queued Context.track_conversion event_count, queued_event_count
tracking.conversion.deduplicated Context.track_conversion reason
tracking.queue.release.started release_queues pending_event_count, batch_size
tracking.queue.release.succeeded release_queues delivered_event_count
tracking.delivery.failed release_queues error_type, remaining_event_count
lookup.goal.completed Context.diagnose_goal resolved, reason
lookup.entity.completed Context.diagnose_config_entity entity_type, resolved

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
├── InitializationError
│   ├── ConfigValidationError  (code: "config.validation_error")
│   └── ConfigLoadError        (code: "config.load_error")
└── TrackingError
    ├── GoalNotFoundError      (code: "goal.not_found")
    ├── ConversionDataError    (code: "conversion.data_error")
    └── TrackingDeliveryError  (code: "tracking.delivery_error")

Example structured error handling:

from convert_sdk import GoalNotFoundError, ConfigLoadError

try:
    context.track_conversion("unknown-goal")
except GoalNotFoundError as exc:
    print(exc.code)                              # "goal.not_found"
    print(exc.context["goal_key"])               # "unknown-goal"
    print(exc.context["available_goal_count"])   # how many goals exist

Diagnostic result objects

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

diag = context.diagnose_experience(
    "checkout-flow",
    location_attributes={"path": "/checkout"},
)

print(diag.resolved)     # bool — True when bucketed
print(diag.reason)       # str — why decision was made
print(diag.message)      # human-readable description
print(diag.result)       # ExperienceResult | None
print(diag.details)      # Mapping with bucket_value, etc.

The same pattern applies to features and goals:

feat_diag = context.diagnose_feature("checkout-banner")
goal_diag = context.diagnose_goal("purchase")
entity_diag = context.diagnose_config_entity("experience", "checkout-flow")
entity_by_id = context.diagnose_config_entity_by_id("experience", "exp-checkout")

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

Experience reason codes

reason Meaning What to check
bucketed Visitor is in the experience and received a variation Variation key is in result
experience_not_found The experience key was not in the config snapshot Verify experience key in the dashboard
experience_inactive Experience status is not active Check experience status in the dashboard
environment_miss The experience does not include the requested environment Check environment in SDKConfig and the experience's environments list
audience_miss Visitor attributes did not satisfy the audience rules Print visitor_attributes and compare to audience rules
location_miss Location attributes did not match the site-area rules Print location_attributes and compare to site-area rules
outside_traffic Bucket value exceeded the total allocated traffic Check traffic_allocation sum in variations; verify bucket_value in details
all_variations_excluded All variations have status set to exclude them Check variation statuses in the dashboard

Feature reason codes

Feature evaluation backs every feature lookup through an experience. The reason codes are the same as the experience codes above, plus:

reason What it means
feature_not_found Key typo or feature not in config
no_backing_experience Feature exists but no experience references it

Cross-SDK comparable fields

The ExperienceDiagnostic.details dict always includes bucket_value when bucketing was attempted. This 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 bucketing 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

bucket = get_bucket_value(
    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

from convert_sdk import Core, SDKConfig, ConfigLoadError

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

print("is_ready:", core.is_ready)
snapshot = core.snapshot
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.

ConfigSnapshot (the type returned by core.snapshot) lives at convert_sdk.domain.config_snapshot and is treated as a stable internal type for support / introspection use. Its field names (experiences_by_key, features_by_key, goals_by_key) are part of that informal contract — they will not be renamed without notice — but they are not re-exported from the top-level convert_sdk package, so import them only from this submodule when you need a type annotation.

Tracking delivery failures

If events are not appearing in the Convert dashboard:

  1. Check flush_result.remaining_event_count — non-zero means delivery was interrupted.
  2. Subscribe to LifecycleEvent.TRACKING_DELIVERY_FAILED to capture the error_type (see Code Examples — Queue Control).
  3. Confirm that release_queues() is called before the process exits or the request completes.
  4. Confirm that sdk_key or account_id/project_id is present in the config snapshot — the transport requires at least one routing identifier.

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 log output — Enable debug logging (above) and capture the output for the failing request.
  5. Diagnostic result — For evaluation or tracking failures, use the diagnose_* methods and include the full reason, message, and details fields.
  6. Error details — If an exception was raised, include exc.code and dict(exc.context).
  7. 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):

import os

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(config_data=config))
context = core.create_context("repro-visitor-1", {"tier": "premium"})
diag = context.diagnose_experience("your-experience-key")
print(diag.resolved, diag.reason, dict(diag.details))

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

What to read next

Clone this wiki locally