Troubleshooting

Common errors and how to fix them

Common Errors

SDK Initialization Failed

  • Cause (JS): Incorrect sdkKey or network issues
  • Cause (PHP): Incorrect sdkKey, network issues, or no PSR-18 HTTP client installed
  • Cause (Android): Incorrect sdkKey or network issues — on a failed config fetch the SDK loads the last cached config from app-private storage; with no cache, decision methods return null until the next successful fetch
  • Cause (Ruby): Incorrect sdk_key (or sdk_key_secret) or network issues — a failed config fetch is logged at warn and decision methods return sentinels rather than raising; ConvertSdk.create itself raises ArgumentError only on misconfiguration (for example, neither sdk_key: nor data: supplied)
  • Cause (iOS): Incorrect sdkKey (or sdkKeySecret) or network issues — on a failed config fetch the SDK loads the last-good config cached on disk (Application Support); with no cache, runExperience returns nil and runFeature returns a disabled Feature until the next successful fetch. ready() is the only throwing call, and it throws only on an unrecoverable config error (empty sdkKey, or empty/invalid direct configData)
  • Cause (Python): Incorrect sdk_key or network issues — unlike the mobile SDKs, Core(SDKConfig(...)).initialize() is fail-fast with no on-disk last-good cache: a malformed config raises InvalidConfigError and a failed fetch raises ConfigLoadError (both subclasses of ConfigError). Wrap initialize() in try / except ConfigError and decide your fallback. Direct-data init makes no network call.
  • Solution: Verify the sdkKey and ensure network connectivity. If using an authenticated key, ensure sdkKeySecret is also provided. (PHP only) Install a PSR-18 HTTP client (e.g., composer require guzzlehttp/guzzle).

No PSR-18 HTTP Client Found (PHP only)

  • Cause: php-http/discovery cannot find a PSR-18 HTTP client implementation
  • Error message: No PSR-18 HTTP client found. Install one (e.g., guzzlehttp/guzzle ^7) or pass an explicit httpClient.
  • Solution: Install one: composer require guzzlehttp/guzzle or any other PSR-18 compatible client.

User Context Creation Failed

  • Cause (JS): Missing or invalid userId
  • Cause (PHP): Missing or empty visitorId, or SDK not initialized
  • Solution (JS): Ensure a valid, non-empty userId string is provided to createContext().
  • Solution (PHP): Ensure a valid, non-empty visitorId string is provided to createContext(). Check that isReady() returns true before creating contexts.
  • Solution (Android): createContext() with no arguments auto-generates and persists a UUID visitor id. Decision calls made before the SDK is ready return null (never throw) — gate them behind onReady { }.
  • Solution (Ruby): A blank or nil visitor_id makes create_context return nil (with an error log) rather than raising — guard for nil before using the context. Subscribe to the ready event (or pass static data:) so contexts are created against an installed config.
  • Solution (iOS): createContext(visitorId:attributes:) with no visitorId auto-generates and persists a Keychain UUID. Decision calls made before try await sdk.ready() return nil / disabled features (never crash) — await sdk.ready() first.
  • Solution (Python): create_context(visitor_id, ...) raises RuntimeError if called before initialize(). Because initialize() is synchronous and returns a ready Core, there is no "created before ready" race to guard against — initialize first, then create contexts. Pass a stable, non-empty visitor_id for deterministic bucketing.

Experience Not Found

  • Cause: Incorrect experienceKey or experience not defined in the project
  • Solution: Verify the experienceKey matches exactly. Ensure the experience is active (not draft or paused) and belongs to the correct environment. To QA a draft or paused experience on purpose, load it with a debug token — see QA & Preview.

Feature Not Found

  • Cause: Incorrect featureKey or feature not linked to any experience
  • Solution: Verify the featureKey. Ensure at least one active experience has a variation with a fullStackFeature change linked to this feature.

Visitor Not Bucketed (RuleError)

  • Cause: The visitor's properties or location don't match the experience's audience/location rules
  • Solution (JS): Check that the visitorProperties and locationProperties passed to the method match the rules configured in Convert. See Rule Evaluation & Targeting for how rules are evaluated.
  • Solution (PHP): Check that the visitorProperties and locationProperties in BucketingAttributes match the rules configured in Convert. The RuleError enum has two cases:
    • RuleError::NoDataFound -- no matching data for the rule
    • RuleError::NeedMoreData -- insufficient data provided to evaluate the rule
  • Solution (Ruby): A miss returns a frozen sentinel instead of a variation — detect it with case variation.key when nil (every sentinel has a nil #key), or check variation.error?. The sentinels are RuleError::NO_DATA_FOUND, RuleError::NEED_MORE_DATA, and BucketingError::VARIATION_NOT_DECIDED. Check that the visitor_properties / location_properties you pass match the rules configured in Convert.
  • Solution (iOS): A miss returns nil from runExperience (and a disabled Feature from runFeature), never an error — a rule mismatch, an inactive or unknown experience, an ineligible visitor, or a not-yet-ready SDK all yield a miss. Check that the scalar attributes you pass to createContext match the rules configured in Convert; only String/Int/Double/Bool values are kept (nested values are dropped).
  • Solution (Python): A miss returns None from run_experience and run_feature (and an empty list from run_experiences / run_features), never an error. For the why without guessing, call context.diagnose_experience(key) / diagnose_feature(key) — they return a typed DiagnosticReason (e.g. AUDIENCE_MISMATCH, EXPERIENCE_NOT_FOUND) and never raise on a miss. Check that the attributes / location_attributes you pass match the rules configured in Convert.

Tracking Events Not Appearing

If requests return 200 OK but nothing appears in Live Logs, see HTTP 200 but No Live Logs below.

  • Cause: Tracking is disabled, events are still queued, or the SDK key is invalid
  • Solution (JS): Ensure network.tracking is not false in the config. Call releaseQueues() to flush pending events. Verify the SDK key. Check that enableTracking is not false in method attributes.
  • Solution (PHP): Ensure network.tracking is not false in the config. Call releaseQueues() or flush() to send pending events. Verify the SDK key. The shutdown handler should flush automatically in PHP-FPM, but in CLI scripts you may need to call flush() explicitly.
  • Solution (Android): Check that tracking wasn't disabled via trackingEnabled(false) at build time, setTrackingEnabled(false) at runtime, or enableTracking = false on the call. Offline events are queued durably and ship when connectivity returns — declare ACCESS_NETWORK_STATE so the SDK can flush immediately on reconnect.
  • Solution (Ruby): Ensure tracking is not false in the config (the global switch always wins over a per-call track). Call flush (alias release_queues) to send pending events. The PID-guarded at_exit handler flushes on process exit, but when flush_interval is set to nil (Lambda / CLI) you must call flush explicitly. Verify enable_tracking is not false in the call attributes.
  • Solution (iOS): Ensure tracking isn't disabled at any of the three layers — static ConvertConfiguration.networkTracking = false, runtime await sdk.setTrackingEnabled(false), or per-call enableTracking: false. Offline events are queued durably in Application Support and ship when connectivity returns; a background URLSession also completes delivery after the app is suspended.
  • Solution (Python): track_conversion (and the bucketing/activation event enqueued by run_experience / run_experiences) only enqueues — it never sends over the network. Events deliver when batch_size (default 10) fills, when the opt-in auto_flush_interval_ms timer fires, on a best-effort atexit hook, or when you call core.flush(). In short-lived runtimes (Lambda / CLI) leave auto_flush_interval_ms unset and call core.flush() explicitly before exit. If experiment exposures aren't appearing, verify you didn't pass enable_tracking=False on the run call — it suppresses the bucketing event per call (default is True). A ConversionStatus.GOAL_NOT_FOUND result (not an exception) means the goal key is absent from the loaded config — verify the sdk_key and the goal key.

HTTP 200 but No Live Logs

The tracking server answers 200 OK even when it drops the events, so a successful response does not prove the events were recorded.

  • Cause: The visitor ID contains a character outside 0-9, a-z, A-Z, ., - and _. A common example is an Auth0 user ID such as auth0|64f1c2…, or an email address.
  • Solution: Pass an ID that matches ^[0-9a-zA-Z._-]+$. If your own ID contains other characters, pass its SHA-256 hex digest instead. See Visitor ID Format.
  • Cause (direct endpoint only): The request uses a default HTTP client User-Agent, so the bot filter drops it.
  • Solution (direct endpoint only): Set a User-Agent that contains ConvertAgent/<version>. See Direct Tracking Endpoint.

Config Validation Failed (PHP only)

  • Cause: The fetched or provided project configuration is malformed
  • Solution: If using static data, verify the structure matches the Convert Serving API format. If using sdkKey, the CDN response may be temporarily invalid -- the SDK will throw a ConfigValidationException.

Did this page help you?