Skip to content
apertio

Learn

A/B testing without injected JavaScript

Most testing tools inject a third-party script that mutates the DOM, the single source of flicker, supply-chain risk, and CSP relaxations. Apertio makes a variant bounded data resolved server-side, so no third-party script ever runs on the page.

Most A/B testing tools work by injecting JavaScript. You add a third-party script to your page, and at runtime that script reads the visitor’s assignment and mutates the live DOM to apply a variant. That injected script is the source of several problems at once. It causes flicker, because the page renders its real content first and the script rewrites it after loading. It is a security concern, because third-party code running on your page is an XSS and supply-chain surface. And it fights a strict Content Security Policy, because some of these tools need unsafe-eval or require you to whitelist their domain, which is exactly what a strict CSP exists to forbid.

The Content Security Policy detail is where this gets concrete for an engineer or a security reviewer. A strong CSP uses a nonce and forbids unsafe-inline and unsafe-eval, so the browser refuses to run inline scripts or evaluate strings as code. A testing tool that injects and evaluates JavaScript at runtime cannot live under that policy without weakening it. Vendors document this: their setup instructions tell you to add their domain to your script-src or to allow unsafe-eval. Every relaxation is a hole you opened in your own policy to fit a marketing tool.

A/B testing without injected JavaScript means moving the decision off the client and out of a runtime script. Apertio does this by making a variant data rather than code. A variant payload is a primitive value or an enum key, never markup, never a URL, never a CSS or JS string. The server, or the edge, reads the assignment cookie, resolves a { slotKey: value } map, and the app’s own React components render those values. No third-party script runs on the page. Apertio never touches the DOM. A strict nonce-based CSP with no unsafe-inline and no unsafe-eval stays intact, because there is nothing to whitelist and nothing to evaluate.

A variant fills these with bounded values

ts
// slots.ts — the experimentable surface, owned by engineering.
// A variant fills these with bounded values; it never ships code to the browser.
import { slot } from '@apertio/core'

export const slots = {
  'home.hero.headline': slot.text({
    label: 'Hero headline',
    default: 'Experiment without injecting a single script',
    maxLength: 60,
  }),
  'home.hero.cta': slot.text({
    label: 'CTA copy',
    default: 'Get started',
    maxLength: 24,
  }),
}

It never ships code to the browser. The experimentable surface, owned by engineering.

There is a second security property that follows from variants being bounded. Because each slot constrains its values to an engineer-approved allowlist, every possible rendered output is enumerable in advance. The schema compiled into the deployed app is the authority on what can render, and the SDK re-validates every value against that embedded schema at resolution time. An out-of-bounds value falls back to the slot’s default and the exposure is flagged as suppressed. A compromised control plane or a stolen API token cannot make the app render something the deployed code did not allow. This is a different posture from a tool whose injected script can run arbitrary code on the page.

The honest limitation applies here too. Only declared slots are testable. A novel surface still needs an engineer to add a slot, which is a small, reviewable change like the snippet above. Apertio converts "an engineer per test" into "an engineer per new surface." That is the trade for keeping injected JavaScript off the page entirely.

No third-party script on the page

The variant is resolved server-side or at the edge. No testing library loads in the browser, so there is no injected JavaScript and no DOM mutation.

Strict CSP stays intact

A nonce-based policy with no unsafe-inline and no unsafe-eval works unchanged. There is nothing to whitelist and nothing to evaluate.

A variant is bounded data

The payload is a primitive or an enum key, never markup, a URL, or a CSS or JS string, so the rendered output is enumerable in advance.

The deployed code is the authority

The SDK re-validates every value against the embedded schema at resolution; out-of-bounds values fall back to the default and flag the exposure as suppressed.

Run experiments your security reviewer can sign off on

Bounded variants, resolved as data, with a strict CSP and no injected JavaScript.

Frequently asked questions

Can you do A/B testing without injected JavaScript?
Yes. Instead of injecting a script that mutates the DOM, you resolve the variant server-side or at the edge and render the result with the app’s own components. Apertio works this way: a variant is bounded data, so no third-party script runs on the page.
Does A/B testing require unsafe-eval or weakening the CSP?
Client-side tools often do, because they inject and evaluate JavaScript at runtime, so their docs ask you to add their domain to script-src or allow unsafe-eval. Apertio does not. A strict nonce-based CSP with no unsafe-inline and no unsafe-eval stays intact, because there is nothing to whitelist and nothing to evaluate.
How is a variant delivered if not through injected code?
As data. The server or edge reads the assignment cookie and resolves a map of slot values, each a primitive or an enum key constrained to an engineer-defined allowlist. The app’s own React components render those values. Apertio never touches the DOM.
What stops a compromised platform from rendering something harmful?
The schema compiled into the deployed app is the authority. The SDK re-validates every value against that embedded schema at resolution. An out-of-bounds value falls back to the slot’s default and flags the exposure as suppressed, so a stolen token cannot make the app render outside the declared bounds.