Skip to content
  • There are no suggestions because the search field is empty.

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:

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.

For most existing Convert Experiences users who already have the Convert Tracking Script installed, the Cookie Hand-Off pattern is the quickest implementation path.

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)

  1. Determine targeting and traffic allocation server/edge-side.
  2. Set a short-lived cookie before any HTML is sent to the browser.
  3. Serve the HTML or redirect accordingly.
  4. 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_sptest before 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_sptest cookie 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_v to respect returning users' existing variations.
  • (Cookie path) Build in your traffic allocation and segmentation targeting.
  • (Cookie path) Set the _conv_sptest cookie 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 _conv_v or are re-assigning them randomly

Parse the _conv_v cookie as shown above so repeat visitors retain their variation. Never manually alter _conv_v!

Wrong HTML

Server/CDN cache doesn't vary by cookie

Ensure your caching layer bypasses cache for _conv_sptest or sets Cache-Control: private