QA & Preview
Preview draft variations with zero-trace preview links, and load QA configs with a debug token
Full Stack projects give you two tools for validating experiments — before and after they go live — without touching production visitors:
- A debug token that lets your SDK load the full QA configuration, including draft and paused experiences, so you can build and test against experiments that aren't serving yet.
- Preview links that render one specific variation of one experiment for a stakeholder — including draft or paused experiments, with no debug token required — bypassing targeting and bucketing entirely and leaving zero trace: no tracking events, no visitor-state writes.
Both are opt-in. Keep the debug token itself out of production builds (see the warning below); preview links are safe to leave wired up anywhere.
Debug Token
By default the SDK receives only the experiences that are actively serving. A debug token widens that: while it is set, the config fetch returns every non-archived experience — drafts and paused ones included — and bypasses the server-side config cache so you always see the latest state.
Generate a debug token from a variation's Preview panel in the Convert app. A token is valid for 24 hours, then expires automatically. Pass it as a configuration option when you create the SDK:
import ConvertSDK from '@convertcom/js-sdk';
const convertSDK = new ConvertSDK({
sdkKey: 'your-sdk-key',
debugToken: 'your-debug-token'
});use ConvertSdk\ConvertSDK;
$sdk = ConvertSDK::create([
'sdkKey' => 'your-sdk-key',
'debugToken' => 'your-debug-token',
]);val sdk = ConvertSDK.builder(applicationContext)
.sdkKey("YOUR_SDK_KEY")
.debugToken("your-debug-token")
.build()CONVERT_SDK = ConvertSdk.create(
sdk_key: "your-sdk-key",
debug_token: "your-debug-token"
)import ConvertSwiftSDK
let sdk = ConvertSwiftSDK(
configuration: ConvertConfiguration(sdkKey: "your-sdk-key", debugToken: "your-debug-token")
)from convert_sdk import Core, SDKConfig
core = Core(SDKConfig(sdk_key="your-sdk-key", debug_token="your-debug-token")).initialize()While the token is set, the SDK also lowers its config cache level so QA changes appear quickly. The token is only ever sent on config requests — never to the tracking endpoint — so treat it as a secret and keep it out of your own logs.
The PHP SDK additionally scrubs the token from its own log output.
Never ship a debug token to production. It exposes unreleased experiments in the config. Keep it in a QA/staging build or behind an environment variable, and remove it before release.
One behavior to keep in mind: a debug config simply contains more experiences — running semantics are unchanged. Calling runExperience with a specific draft key runs that draft with normal bucketing and rules. But a run-all call (runExperiences) will now also evaluate the drafts and paused experiences the token exposed. In QA, run experiences by explicit key so a run-all call doesn't evaluate every draft — and disable tracking (or use a preview link) when you don't want events recorded against the draft you are running.
Preview Links
A preview link renders one specific variation of one experiment for whoever opens it — a designer, a PM, a client — regardless of targeting, status, or traffic. It is the Full Stack equivalent of previewing a draft page.
A preview link needs no debug token, even for an experiment that isn't serving yet. When the previewed experience isn't in the config the SDK already holds, the SDK fetches just that one experience on demand, so a preview link works on a normal production build for any experiment, whatever its status. The debug token is a separate tool for the broader case — loading every draft and paused experience into the config at once for in-code QA (see above).
A preview link carries a single parameter:
?convert_preview={experienceId}.{variationId}
where {experienceId} and {variationId} are the numeric IDs of the experience and the variation you want to show. Convert generates the link for you — copy it from the variation's menu, or render it as a QR code for mobile. Opening that link:
- Forces the requested variation when you run that experiment by key (
runExperience) — skipping audiences, locations, environment, experiment and variation status, bucketing, and any stored decision. Other experiments you run on the same context still render normally but record nothing (see below). - Leaves zero trace. No bucketing or conversion events are tracked, and no visitor state is written, for the entire preview session. Every other experiment on the page still evaluates and renders normally — the page stays coherent — but nothing from the preview is recorded.
- Is inert on bad input. An unknown experience or variation ID is ignored (with a warning) and the visitor sees the normal experience.
Wire it up once
Your application reads the convert_preview value from the incoming request or link, and hands it to the SDK before the experience renders. Each SDK ships a helper that parses the {experienceId}.{variationId} value plus a setPreview method on the context — set it up once and every future preview link works with no further changes. The parser returns nothing (null / None / nil) for a missing or malformed value, so the guard falls straight through to normal behavior. (The convert_preview parameter name is a convention your app reads; the SDK parses only the value.)
In the browser — read it from the page URL:
import ConvertSDK, { parsePreviewParam } from '@convertcom/js-sdk';
const preview = parsePreviewParam(
new URLSearchParams(window.location.search).get('convert_preview') ?? ''
);
if (preview) {
await context.setPreview(preview); // { experienceId, variationId }
}On the server — read it from the request. Query parameters don't survive navigation, so back them with a short-lived cookie: when the parameter is present, set the cookie; on every request, read the parameter or the cookie and pass whichever exists to setPreview.
// Express-style: fall back to a short-lived cookie so the preview survives navigation
const raw = req.query.convert_preview ?? req.cookies?.convert_preview;
if (req.query.convert_preview) {
res.cookie('convert_preview', req.query.convert_preview, { maxAge: 30 * 60 * 1000 });
}
const preview = parsePreviewParam(raw);
if (preview) await context.setPreview(preview);use ConvertSdk\Preview\PreviewParam;
$raw = $_GET['convert_preview'] ?? $_COOKIE['convert_preview'] ?? '';
if (!is_string($raw)) { $raw = ''; } // ?convert_preview[]=… would arrive as an array
if (is_string($_GET['convert_preview'] ?? null)) {
setcookie('convert_preview', $_GET['convert_preview'], time() + 1800, '/');
}
if (($parsed = PreviewParam::parse($raw)) !== null) {
$context->setPreview($parsed['experienceId'], $parsed['variationId']);
}# Rails-style (params / cookies / ActiveSupport durations)
raw = params[:convert_preview] || cookies[:convert_preview]
cookies[:convert_preview] = { value: params[:convert_preview], expires: 30.minutes.from_now } if params[:convert_preview]
if (pair = ConvertSdk.parse_preview_param(raw))
context.set_preview(experience_id: pair[0], variation_id: pair[1])
end# Flask-style
from convert_sdk import parse_preview_param
raw = request.args.get("convert_preview") or request.cookies.get("convert_preview") or ""
pair = parse_preview_param(raw)
if pair:
context.set_preview(pair[0], pair[1])
# Persist across navigation: when the param is present, set a ~30-min cookie on the
# response — e.g. resp.set_cookie("convert_preview", request.args["convert_preview"], max_age=1800)In a mobile app — the preview parameter rides a deep link (or universal / app link). The Convert UI can render the link as a QR code: a stakeholder scans it with their phone and your app opens straight into the previewed variation on a real device, no developer build required. Handle the incoming link, then call setPreview before the screen renders.
import com.convert.sdk.core.preview.PreviewParam
// In the Activity that receives the deep link
val raw = intent.data?.getQueryParameter("convert_preview")
val parsed = raw?.let { PreviewParam.parse(it) }
if (parsed != null) {
context.setPreview(parsed.first, parsed.second) // (experienceId, variationId)
}import ConvertSwiftSDK
// .onOpenURL / scene URL handlers are synchronous — hop into a Task for the async
// setPreview, and render after it resolves so the preview leaves zero trace.
if let raw = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "convert_preview" })?.value,
let parsed = PreviewParam.parse(raw) {
Task { await context.setPreview(experienceId: parsed.experienceId, variationId: parsed.variationId) }
}If your app already handles deep links, a preview link is just another query parameter — nothing new to register. If it doesn't, add deep-link support once (Android intent filter, or an iOS URL scheme / Associated Domains); after that, every preview link works out of the box.
Updated 1 day ago