A/B testing avec Claude Code pour SaaS et monétisation de blog
Concevez des tests A/B fiables avec Claude Code: hypothèse, événements, split serveur, SQL, consentement et rollback.
Commencer par l’hypothèse, pas par le composant
Un test A/B n’est pas seulement une alternance entre deux interfaces. Pour un SaaS ou un blog monétisé, c’est une manière contrôlée de vérifier si un changement améliore une inscription, un clic d’intention d’achat, un lien affilié, un revenu publicitaire ou une demande de conseil sans détériorer le reste du parcours. Claude Code peut générer le composant rapidement, mais l’expérience ne sera utile que si l’hypothèse, les événements, la taille d’échantillon, les métriques de garde-fou, la confidentialité et le rollback sont définis.
En langage simple: une variante est la version testée; une exposition est le moment où l’utilisateur voit une variante et où l’on l’enregistre; un garde-fou est une métrique qui ne doit pas empirer, comme LCP, le taux d’erreurs ou les clics d’intention d’achat; un faux positif est une amélioration apparente due au hasard.
Le prompt de départ doit donc parler métier:
Conçois un workflow de test A/B pour un SaaS/blog en Next.js App Router.
L'objectif est la monétisation, pas les clics de vanité.
experiment id: pricing_page_offer_2026_06
hypothèse: remplacer le CTA pricing "Start free trial" par "Start with the free plan" augmente les débuts d'inscription sans réduire les clics d'intention d'achat.
métrique principale: signup_start_rate
garde-fous: purchase_link_click_rate, p75 LCP, JavaScript error rate
livrables: schema d'événements, assignation serveur, limites Cookie/localStorage, SQL type BigQuery, vérification Playwright, checklist rollout/rollback.
Prépare au moins trois cas concrets: CTA de page pricing SaaS, position d’un bloc affilié dans un article, formulaire de newsletter, étape d’onboarding ou bloc de réservation de consultation. Pour le socle de feature flags, consulte feature flags avec Claude Code; pour la mesure, analytics avec Claude Code.
| Cas | Métrique principale | Garde-fous | Échec courant |
|---|---|---|---|
| CTA pricing SaaS | Débuts d’inscription | Clics d’achat, erreurs, LCP | Plus d’inscriptions mais moins de qualité commerciale |
| Bloc affilié de blog | Clics produit | Lecture complète, rebond, vitesse | Le bloc arrive trop tôt et abîme la confiance |
| Formulaire newsletter | Inscriptions terminées | Spam, désabonnements | Volume en hausse, qualité en baisse |
| Onboarding | Premier succès | Tickets support, activation | Le court terme cache le churn |
Figer le schéma d’événements avant l’UI
L’erreur coûteuse est de découvrir après lancement que les données ne se rejoignent pas. Si le même clic s’appelle button_click, ctaClicked et signup_click, l’analyse devient du nettoyage manuel. Demandez à Claude Code un contrat d’événements typé avant les composants. Pour Google Analytics, vérifiez la référence officielle GA4 events et la référence Google tag parameters.
// lib/experiment-events.ts
export type ExperimentId = "pricing_page_offer_2026_06";
export type VariantId = "control" | "free_plan_copy";
export type ExperimentEvent =
| {
event_name: "experiment_exposure";
experiment_id: ExperimentId;
variant: VariantId;
anonymous_id: string;
page_path: string;
}
| {
event_name: "cta_click";
experiment_id: ExperimentId;
variant: VariantId;
anonymous_id: string;
cta_id: "pricing_primary" | "article_bottom" | "sidebar_offer";
page_path: string;
}
| {
event_name: "purchase_link_click";
experiment_id: ExperimentId;
variant: VariantId;
anonymous_id: string;
product_id: string;
value_usd: number;
page_path: string;
}
| {
event_name: "guardrail_metric";
experiment_id: ExperimentId;
variant: VariantId;
anonymous_id: string;
metric_name: "lcp_ms" | "js_error" | "bounce";
value: number;
page_path: string;
};
declare global {
interface Window {
gtag?: (command: "event", name: string, params: Record<string, unknown>) => void;
}
}
export function trackExperimentEvent(event: ExperimentEvent) {
if (typeof window === "undefined") return;
window.gtag?.("event", event.event_name, {
experiment_id: event.experiment_id,
variant: event.variant,
anonymous_id: event.anonymous_id,
page_path: event.page_path,
...event,
});
}
N’envoyez pas d’e-mails, de noms, de sociétés ou de texte libre dans les événements. Si votre marché exige un consentement pour l’analytics ou la publicité, initialisez l’état de consentement avant l’envoi des tags. Google le décrit dans le guide officiel consent mode.
Assigner les variantes côté serveur
localStorage est tentant, mais il peut provoquer un clignotement au premier rendu, des variantes différentes avant/après connexion, des réinitialisations en navigation privée et des problèmes de stockage bloqué. MDN décrit localStorage comme un stockage par origine persistant entre sessions, voir MDN localStorage. Ce n’est pas une source fiable pour le premier rendu.
Avec Next.js App Router, un Route Handler suffit pour démarrer. La documentation officielle route.ts explique les handlers basés sur les Web Request/Response APIs. Les cookies peuvent être définis via NextResponse. Si vous devez agir au bord du réseau, notez que Next.js 16 a renommé Middleware en Proxy; consultez proxy.js.
// app/api/experiments/assign/route.ts
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge";
type Variant = "control" | "free_plan_copy";
const EXPERIMENTS = {
pricing_page_offer_2026_06: {
cookieName: "ab_pricing_page_offer_2026_06",
variants: [
{ id: "control", weight: 50 },
{ id: "free_plan_copy", weight: 50 },
] satisfies Array<{ id: Variant; weight: number }>,
},
};
function hashToBucket(input: string) {
let hash = 2166136261;
for (let index = 0; index < input.length; index += 1) {
hash ^= input.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return Math.abs(hash) % 100;
}
function chooseVariant(experimentId: keyof typeof EXPERIMENTS, anonymousId: string): Variant {
const experiment = EXPERIMENTS[experimentId];
const bucket = hashToBucket(`${experimentId}:${anonymousId}`);
let cumulative = 0;
for (const variant of experiment.variants) {
cumulative += variant.weight;
if (bucket < cumulative) return variant.id;
}
return experiment.variants[0].id;
}
export async function GET(request: NextRequest) {
const experimentId = request.nextUrl.searchParams.get("experiment");
if (experimentId !== "pricing_page_offer_2026_06") {
return NextResponse.json({ error: "Unknown experiment" }, { status: 404 });
}
const experiment = EXPERIMENTS[experimentId];
const testAnonymousId = request.headers.get("x-test-anonymous-id");
const existingCookie = request.cookies.get(experiment.cookieName)?.value;
const anonymousId = testAnonymousId ?? existingCookie ?? crypto.randomUUID();
const variant = chooseVariant(experimentId, anonymousId);
const response = NextResponse.json({
experimentId,
variant,
anonymousId,
});
response.cookies.set(experiment.cookieName, anonymousId, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 24 * 30,
});
return response;
}
Les cookies demandent aussi une configuration prudente. Le guide MDN secure cookie configuration couvre Secure, HttpOnly et SameSite. Un SaaS connecté peut utiliser un identifiant utilisateur haché; un blog public préférera un cookie anonyme court; la publicité doit respecter le CMP et les règles locales.
Séparer l’expérience du rollout
Le code peut être déployé, mais l’exposition doit rester contrôlable. Vercel propose Vercel Flags; une configuration YAML suffit souvent au départ.
# config/experiments.yaml
experiments:
pricing_page_offer_2026_06:
status: running
owner: masa
hypothesis: "Free-plan copy increases signup starts without hurting paid intent."
allocation_percent: 50
variants:
control: 50
free_plan_copy: 50
primary_metric: signup_start_rate
guardrails:
- purchase_link_click_rate
- p75_lcp_ms
- js_error_rate
rollback:
if_js_error_rate_increases_by: 0.02
if_p75_lcp_ms_worse_by_ms: 300
action: "set allocation_percent to 0 and keep logging exposure for audit"
Écrivez le rollback avant le lancement. Si les erreurs montent, si LCP se dégrade ou si les clics d’intention d’achat baissent, coupez l’exposition et gardez les logs. Passez ensuite de 10% à 50%, puis 100% seulement si la métrique principale et les garde-fous tiennent.
Analyser depuis l’exposition sans se tromper
Le dénominateur doit être l’exposition. Les utilisateurs qui n’ont pas vu de variante ne comptent pas. Ceux qui ont vu deux variantes doivent être exclus ou investigués. BigQuery fournit SAFE_DIVIDE pour éviter les divisions par zéro.
-- BigQuery Standard SQL
WITH exposure_raw AS (
SELECT
anonymous_id,
experiment_id,
ARRAY_AGG(variant ORDER BY event_timestamp LIMIT 1)[OFFSET(0)] AS variant,
MIN(event_timestamp) AS first_exposed_at,
COUNT(DISTINCT variant) AS variant_count
FROM `project.dataset.events`
WHERE event_name = 'experiment_exposure'
AND experiment_id = 'pricing_page_offer_2026_06'
GROUP BY anonymous_id, experiment_id
),
exposure AS (
SELECT anonymous_id, experiment_id, variant, first_exposed_at
FROM exposure_raw
WHERE variant_count = 1
),
events_after_exposure AS (
SELECT
e.variant,
e.anonymous_id,
ev.event_name,
ev.value_usd,
ev.value_ms
FROM exposure e
LEFT JOIN `project.dataset.events` ev
ON ev.anonymous_id = e.anonymous_id
AND ev.experiment_id = e.experiment_id
AND ev.event_timestamp >= e.first_exposed_at
)
SELECT
variant,
COUNT(DISTINCT anonymous_id) AS exposed_users,
COUNT(DISTINCT IF(event_name = 'cta_click', anonymous_id, NULL)) AS cta_users,
SAFE_DIVIDE(
COUNT(DISTINCT IF(event_name = 'cta_click', anonymous_id, NULL)),
COUNT(DISTINCT anonymous_id)
) AS cta_click_rate,
COUNT(DISTINCT IF(event_name = 'purchase_link_click', anonymous_id, NULL)) AS purchase_intent_users,
SAFE_DIVIDE(
COUNT(DISTINCT IF(event_name = 'purchase_link_click', anonymous_id, NULL)),
COUNT(DISTINCT anonymous_id)
) AS purchase_intent_rate,
AVG(IF(event_name = 'guardrail_metric' AND value_ms IS NOT NULL, value_ms, NULL)) AS avg_guardrail_ms,
SUM(IF(event_name = 'guardrail_metric' AND value_usd IS NOT NULL, value_usd, 0)) AS revenue_proxy_usd
FROM events_after_exposure
GROUP BY variant
ORDER BY variant;
La taille d’échantillon doit être décidée avant le lancement. Regarder le tableau chaque jour et arrêter dès que la nouvelle version mène augmente le risque de faux positif. Trop de variantes, trop de segments, une métrique principale changée après coup ou une campagne payante lancée en même temps peuvent aussi biaiser le résultat.
Vérifier avec Playwright
Avant publication, vérifiez que le même identifiant anonyme reçoit la même variante, qu’un experiment id inconnu renvoie une erreur et que le CTA de monétisation n’apparaît qu’une fois. Playwright documente test et expect, ainsi que les assertions avec retry automatique.
// tests/experiments.spec.ts
import { test, expect } from "@playwright/test";
test.describe("pricing_page_offer_2026_06", () => {
test("keeps assignment stable for the same anonymous id", async ({ request, baseURL }) => {
const url = `${baseURL}/api/experiments/assign?experiment=pricing_page_offer_2026_06`;
const headers = { "x-test-anonymous-id": "demo-user-42" };
const first = await request.get(url, { headers });
const second = await request.get(url, { headers });
expect(first.ok()).toBeTruthy();
expect(second.ok()).toBeTruthy();
expect(await first.json()).toMatchObject(await second.json());
});
test("rejects unknown experiments", async ({ request, baseURL }) => {
const response = await request.get(`${baseURL}/api/experiments/assign?experiment=missing`);
expect(response.status()).toBe(404);
});
test("renders one monetization CTA on the pricing page", async ({ page }) => {
await page.goto("/pricing?e2e_anonymous_id=demo-user-42");
await expect(page.getByTestId("pricing-cta")).toBeVisible();
await expect(page.getByTestId("pricing-cta")).toHaveCount(1);
});
});
Dans les essais de Masa sur des CTA d’article, le meilleur gain venait du tableau d’événements préparé avant le code. Définir les clics de monétisation, le moment de l’exposition et le traitement des utilisateurs multi-variantes a réduit les retours à Claude Code. Un test Playwright a aussi révélé un clignotement lié à localStorage, corrigé par l’assignation serveur.
Pour appliquer cela à un vrai funnel, consultez Claude Code training et product templates. Le but n’est pas d’empiler les expériences, mais de répondre proprement à une question de revenu sans perdre la confiance du lecteur.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.