Server Side Testing
Run your A/B or Split-URL tests before any HTML leaves your server or CDN while maintaining accurate analytics in Convert.
| Author: | George F. Crew |
THIS ARTICLE WILL HELP YOU:
- Know why to use Server Side Testing
- Show Supported Platforms
- Understand Pre-requisites
- Understand Patterns
- Get a Checklist
- Know how to Troubleshoot
Why Use Server-Side Testing?
-
Minimize content flash for server-rendered pages
-
Fully control variation logic backend-side or at the edge.
-
Preserve experiment tracking integrity
- Run tests where the variation must be decided before the browser receives the page
- Use backend logic, request data, or CDN edge logic to decide which experience a visitor receives
Supported Patterns
| Pattern | Use Case |
|---|---|
Cookie hand-off (_conv_sptest + Convert Tracking Script) |
You already use the Convert Tracking Script and need early rendering or redirects using your server (PHP) or CDN Edge Workers. |
| Convert SDKs (no tracking script) |
You want complete server-side control over bucketing and tracking using Convert's Full Stack SDKs. |
Prerequisites
| Requirement | Cookie Hand-Off | SDKs |
|---|---|---|
Set cookies (_conv_sptest) |
✅ | ❌ |
| Backend Environment / Edge Worker | ✅ | ✅ |
| Convert Tracking Script | ✅ | ❌ |
| Public SDK key | ❌ | ✅ |
| Stable visitor ID | Recommended (_conv_v) |
✅ |
Pattern A – Cookie Hand-Off (with Tracking Script)
- Determine targeting and traffic allocation server/edge-side.
- Set a short-lived cookie before any HTML is sent to the browser.
- Serve the HTML or redirect accordingly.
- The Convert Tracking Script reads this cookie on load and tracks appropriately.
⚠️ IMPORTANT: Targeting & Location Conditions for _conv_sptest
Experiments triggered with the _conv_sptest cookie need to be targeted to the right page and visitors server-side using your own code.
Because targeting, variation assignation, and traffic allocation will be done entirely server-side, the related test in Convert must have an unmatchable condition on its Location conditions.
- Recommendation: Set a JS Condition with a value of false. This prevents the client-side Convert script from attempting to bucket users or override your server-side logic.
This is especially important when using PHP or CDN logic to assign visitors before the page loads.
Setting Up the Convert Experiment for Cookie Hand-Off
Before adding your server-side code, create a regular Convert A/B experiment.
To make the experiment savable, you can add a small placeholder change in the Visual Editor. For example, go to:
Variation Menu → Custom JavaScript
Then add:
// This is the variation.
This allows the experiment to exist in Convert, collect data, and display results in the report, while your backend code handles the actual variation rendering.
You will also need to collect the following IDs before implementing the server-side logic:
- Project ID / Project Number
- Experiment ID
- Original variation ID
- Challenger variation ID or IDs
The PHP example below is intended as a practical cookie hand-off implementation. Adapt the targeting rules, domain, cookie settings, and variation output to match your production environment.
PHP Example: Targeting, Allocation, and Cookie Hand-off
This example checks the _conv_v tracking cookie to see if the visitor is already bucketed. If they aren't, it targets visitors on the /pricing page who arrive via the summer_sale UTM campaign, splits them evenly (50/50), and hands the assignment to the browser.
<?php
$expId = '123456789'; // Your Experiment ID
$controlId = '111111111'; // Original Variation ID
$varId1 = '987654321'; // Challenger Variation ID
$assignedVarId = null;
// 1. Read the _conv_v cookie to see if the user is ALREADY bucketed in this experiment
if (isset($_COOKIE['_conv_v'])) {
if (preg_match('/' . $expId . '\.\{v\.([0-9]+)/', $_COOKIE['_conv_v'], $matches)) {
$assignedVarId = $matches[1]; // Extract existing variation ID
}
}
// 2. Target a specific URL path
$requestPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$isTargetPage = ($requestPath === '/pricing');
// 3. Target a specific traffic segment (utm_campaign)
$isTargetCampaign = (isset($_GET['utm_campaign']) && $_GET['utm_campaign'] === 'summer_sale');
// 4. If the user isn't bucketed yet, verify targeting rules and assign a variation
if (!$assignedVarId && $isTargetPage && $isTargetCampaign) {
// Simple Traffic Allocation (50/50 split)
$assignedVarId = (rand(1, 100) <= 50) ? $varId1 : $controlId;
// Set the _conv_sptest cookie (expires in 10 seconds)
setcookie('_conv_sptest', "{$expId}:{$assignedVarId}", time() + 10, '/', '', false, false);
}
// 5. Serve the corresponding variation HTML
if ($assignedVarId === $varId1) {
echo "<h1>Welcome to the Summer Sale Pricing!</h1>";
} else {
echo "<h1>Standard Pricing Plan</h1>";
}
?>
Important implementation notes:
- Set
_conv_sptestbefore any HTML output or headers are sent. - Do not manually overwrite or edit
_conv_v. Read it only to respect existing variation assignments. - Keep the
_conv_sptestcookie short-lived. It is only needed for the hand-off to the Convert Tracking Script. - Your PHP code should own targeting, segmentation, and allocation logic.
- The Convert experiment should use an unmatchable Location condition, such as a JS Condition set to
false.
Alternative PHP Example: Reading Project JSON
In some cases, you may want your PHP code to read the project JSON and select from the available variation IDs. This can work, but it is usually better to hardcode known experiment and variation IDs in production to avoid adding latency to the request.
Fetching project data dynamically can introduce request latency. Use it only when your implementation requires it, and consider caching the response server-side.
<?php
define('PROJECTNUMBERID', '10014852_10015867');
$experimentId = '100122624';
$selectedVariation = null;
$currentTime = time();
if (!isset($_COOKIE['_conv_v'])) {
// Get the variations from the Convert project JSON
$projectDataUrl = 'https://cdn-3.convertexperiments.com/JSON/' . PROJECTNUMBERID . '.json';
$output = file_get_contents($projectDataUrl);
if ($output) {
$projectData = json_decode($output, true);
if (
isset($projectData['experiments'][$experimentId]) &&
isset($projectData['experiments'][$experimentId]['vars_sort'])
) {
$variations = $projectData['experiments'][$experimentId]['vars_sort'];
if (count($variations) > 0) {
$selectedVariation = $variations[array_rand($variations, 1)];
}
}
}
} else {
// Read _conv_v to check whether the visitor is already bucketed
$convertCookie = (string) $_COOKIE['_conv_v'];
if (preg_match('/' . $experimentId . '\.\{v\.([0-9]+)/', $convertCookie, $matches)) {
$selectedVariation = $matches[1];
}
}
if ($selectedVariation) {
setcookie(
'_conv_sptest',
"{$experimentId}:{$selectedVariation}",
$currentTime + 10,
'/',
'',
false,
false
);
}
if ($selectedVariation === '1001176189') {
echo 'First Variation Selected';
} elseif ($selectedVariation === '1001176190') {
echo 'Second Variation Selected';
} else {
echo 'Original Variation Selected';
}
?>
Use this version only if you need dynamic variation lookup. For most production use cases, the earlier PHP example with explicit experiment and variation IDs is simpler, faster, and easier to QA.
CDN Edge Examples (JavaScript)
You can run the same logic directly on your CDN to intercept requests and rewrite responses or redirect users before they ever hit your origin server.
Cloudflare Workers:
export default {
async fetch(request) {
const expId = '123456789';
const controlId = '111111111';
const varId1 = '987654321';
const cookieHeader = request.headers.get('Cookie') || '';
// 1. Read the _conv_v cookie to check for existing assignment
const match = cookieHeader.match(new RegExp(expId + '\\.\\{v\\.([0-9]+)'));
let assignedVarId = match ? match[1] : null;
let headersToSet = { 'Cache-Control': 'private' };
// 2. Allocate if not already bucketed
if (!assignedVarId) {
// Simple 50/50 allocation
assignedVarId = (Math.random() < 0.5) ? varId1 : controlId;
// 3. Hand-off cookie for the Convert Tracking Script
headersToSet['Set-Cookie'] = `_conv_sptest=${expId}:${assignedVarId}; Max-Age=10; Path=/; SameSite=Lax`;
}
// 4. Render corresponding content (or modify URL)
const html = await renderVariant(assignedVarId); // Your custom render function
return new Response(html, {
headers: headersToSet
});
}
}
AWS Lambda@Edge (Viewer Request):
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const expId = '123456789';
const controlId = '111111111';
const varId1 = '987654321';
let cookieString = headers.cookie ? headers.cookie[0].value : '';
// 1. Read the _conv_v cookie to check for existing assignment
const match = cookieString.match(new RegExp(expId + '\\.\\{v\\.([0-9]+)'));
let assignedVarId = match ? match[1] : null;
// 2. Allocate if not already bucketed
if (!assignedVarId) {
assignedVarId = (Math.random() < 0.5) ? varId1 : controlId;
// 3. Hand-off cookie for the Convert Tracking Script
request.headers['set-cookie'] = [{
key: 'Set-Cookie',
value: `_conv_sptest=${expId}:${assignedVarId}; Max-Age=10; Path=/; SameSite=Lax`
}];
}
// Note: For Split-URL tests in Lambda@Edge, you would rewrite request.uri
// here based on the assignedVarId before sending the request to your origin.
return request;
};
Checklist
- Create the experiment and obtain
experimentId/variationId. - Choose the Cookie Hand-off.
- Implement server/edge logic in your PHP app or CDN.
- (Cookie path) Build in a way to read
_conv_vto respect returning users' existing variations. - (Cookie path) Build in your traffic allocation and segmentation targeting.
- (Cookie path) Set the
_conv_sptestcookie before any headers or HTML are sent. - (Cookie path) Set Location Conditions to an unmatchable JS Condition (e.g.,
false) in the Convert app to allow your code to handle targeting/allocation. - QA: Use the Convert Chrome Debugger Extension to verify bucketing.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No data in reports (cookie path) |
Cookie not present before tracking script |
Ensure cookie writing logic runs before any HTML output |
|
Visitor switches variation |
You overwrote |
Parse the |
|
Wrong HTML |
Server/CDN cache doesn't vary by cookie |
Ensure your caching layer bypasses cache for |