Common problems
Nothing shows when I present a placement
Nothing shows when I present a placement
A present call that returns without showing anything almost always means FlowPilot had no flow to show, or the SDK was never set up. Work through these in order:
- The SDK is not configured. If
FlowPilot.configure(...)never ran (or ran with a key that does not start withfp_),FlowPilot.sharedisnilandFlowPilot.shared?.presentPlacement(...)silently does nothing. Check the log forFlowPilot SDK initialized - v1.0.0. If you seeInvalid API key formatinstead, the key is wrong. See Configuration. - Wrong App ID. The flow, placement, and key must all belong to the same app. A mismatched
appIdresolves against the wrong app and finds nothing. - The placement has no flow for this user. The placement may be paused, have no default flow attached, or exclude this user by audience targeting or frequency rules. The resolve returns “no flow” and you get a
flowNotFound(ortargetingNotMet/frequencyLimitReached) error. This is expected, not a bug. Check the placement in the dashboard. See Placements and Audience targeting. - The flow is a draft, or is not attached. Only a published flow that is attached to the placement is served. Publishing alone does not attach it. See Publishing.
- The platform excludes iOS. A placement gates which platforms it serves. If iOS is not included, the resolve returns no flow for an iOS device.
- Offline with a cold cache. With no network, no cached flow, and no bundled default, there is nothing to present and the resolve throws.
.debug logging and read which step fails. For anything user-facing, use the never-throwing overload so the user always sees something:The flow never appears and the SDK seems uninitialized (`sdk_not_initialized`)
The flow never appears and the SDK seems uninitialized (`sdk_not_initialized`)
FlowPilot.shared is nil. Causes:FlowPilot.configure(...)was never called.- It was called with an API key that does not start with
fp_. The SDK logsInvalid API key formatand leavessharedasnil. - A present or resolve call ran before
configure(...)finished (for example, from a view that loads earlier than your app’s launch code).
FlowPilot.configure(...) exactly once, as early as possible (the SwiftUI App initializer or application(_:didFinishLaunchingWithOptions:)), before any present or resolve. See Configuration and Quickstart.`invalid_api_key` when resolving
`invalid_api_key` when resolving
The server rejected the key with a
401 during a resolve. Common causes:- Wrong key type. You must use a workspace SDK key, not a dashboard or secret key. See API keys.
- Wrong workspace or app. The key belongs to a different workspace than the flow and placement.
- Wrong environment. A key minted for one environment will not work against another. Match
environmentinFlowPilotConfigurationto where the key and flows live.
appId matches, and confirm environment points at the right backend. Note that a key with the wrong prefix never gets this far: it fails the fp_ check at configure(...) time instead.`timeout` or very slow first present on poor networks
`timeout` or very slow first present on poor networks
The live resolve did not finish within
resolveTimeout (default 4.0 seconds) and there was nothing cached or bundled to fall back to, so the call threw timeout.Fixes, in order of impact:- Ship a bundled default flow so the first present always has something offline. See Offline and bundled flows.
- Prefetch at launch so the placement is warm before the user reaches it. Declare it once in config with
prefetchOnLaunch: ["onboarding"], or callawait FlowPilot.shared?.prefetch(["onboarding"])(addwarmMedia: trueto also warm first-screen images). See Prefetching. - Use the never-throwing present overload (
fallback:) so a timeout falls through to cache, then bundled, then your own UI instead of throwing. - Raise
resolveTimeoutonly if your audience is on consistently slow networks. It bounds the entire resolve, so a higher value means a longer possible wait. The minimum is0.5.
A custom component renders blank (`custom_component_not_found`)
A custom component renders blank (`custom_component_not_found`)
The flow references a custom component the SDK could not find, so it shows a placeholder instead of your view. Causes:
- Not registered. You never called
registerCustomComponent(key:version:definition:), or you registered it after presenting. Register before you present. - Key mismatch. The
keyyou registered does not match the component key in the flow exactly (it is case-sensitive). - Version mismatch. The SDK matches on
key + version. If the flow uses version2and you registered version1, it will not match (a version-1registration also answers the bare, unversioned key, but not other versions).
Fonts look wrong or fall back to the system font
Fonts look wrong or fall back to the system font
A flow renders with the wrong typeface when the SDK cannot resolve the font:
- The font was not uploaded. Custom fonts must be uploaded to the workspace in the dashboard so the resolve response includes them. See Custom fonts.
- Family, weight, or style mismatch. The flow references a family/weight/style combination that does not exist among the uploaded variants, so it falls back to the system font.
- Offline with no bundled font assets. A bundled (offline) flow only has its fonts when you ship them in the asset bundle. Without a
fontsentry in the.flowassetsmanifest, an offline render uses the system font.
Images do not load when offline
Images do not load when offline
Online, images stream from the CDN and get cached. Offline, an image only appears if its bytes are already in the cache or shipped with the flow.
- No bundled assets. A bundled default flow with no image assets shows blank images offline. Ship the images in the
.flowassetsfolder and declare them inmanifest.jsonso the SDK seeds the image cache before first paint. - Cold cache. If the user has never seen the flow online, nothing is cached yet. Preload or prefetch while the network is available.
I republished the flow but the app still shows the old version
I republished the flow but the app still shows the old version
Two things can keep an old flow on screen:
- Cache TTL. The SDK serves a fresh cached flow without hitting the network until it goes stale. The freshness window comes from the server’s
cache_ttl_seconds(the SDK defaults to 300 seconds when the server omits it). Until then, the cached copy is used. CallFlowPilot.shared?.clearCache()to force a re-resolve during testing. - The placement still points at the old version. Publishing a new flow version does not automatically repoint a placement. Until you attach the new version to the placement, the resolve keeps returning the old one. This is the most common cause. See Publishing.
An experiment user keeps switching variants
An experiment user keeps switching variants
Variant assignment is deterministic and sticky: the backend buckets a user by their user id, so the same user gets the same variant every time. If a user keeps flipping variants, their user id is not stable.The SDK generates a stable per-install user id and stores it in the Keychain, so it normally survives app restarts. Switching usually means that id is being reset: a fresh install on a wiped simulator, a cleared Keychain, or testing across multiple devices. There is no public API to set a custom user id in v1.0.0, so you cannot pin it yourself yet.Fix: test variant stickiness on a real device or a persistent simulator without resetting it between launches. See Variables and SDK context and Reading experiment results.
How do I get more diagnostic output?
How do I get more diagnostic output?
Raise the log level. The SDK logs the resolve path, the delivery source, font and image loading, and validation failures.At runtime you can also call
FlowPilot.shared?.enableDebugOverlay() (raises the level to .verbose) and FlowPilot.shared?.logState() to print the current user id, session id, environment, and registered component counts. Set FlowPilot.debugBordersEnabled = true to draw borders around rendered components when debugging layout.When you cannot reproduce it
- Test the resilient path. Turn on Airplane Mode and present. With the never-throwing overload and a bundled flow, the user should still see something. If they see a blank screen, you are missing a bundled default or its assets.
- Confirm the delivery source. The
delivery_sourceon analytics events (and the debug logs) tells you whether a render came from the network, cache, stale cache, or bundled default. A surprising source often explains a surprising flow. - Check the dashboard first. Most “nothing shows” cases are a paused placement, an unattached flow, or a targeting rule, not an SDK bug.