Tips & Tricks (Actualizado: 2/6/2026)

A/B testing con Claude Code para SaaS y monetización de blogs

Diseña tests A/B con Claude Code: hipótesis, eventos, asignación en servidor, SQL, privacidad, guardrails y rollback.

A/B testing con Claude Code para SaaS y monetización de blogs

Empieza por la hipótesis, no por el botón

Un test A/B no consiste en enseñar dos pantallas al azar. Para un SaaS o un blog monetizado, sirve para responder una pregunta de negocio: si un cambio mejora registros, intención de compra, clics de afiliado, ingresos publicitarios o reservas sin romper el resto del embudo. Claude Code puede escribir el cambio visual muy rápido, pero un experimento útil necesita hipótesis, asignación estable, eventos medibles, tamaño de muestra, métricas de seguridad, privacidad y una forma clara de volver atrás.

Traducido a términos simples: una variante es la versión que se compara; una exposición es el momento en que un usuario ve una variante y queda registrado; un guardrail es una métrica que no debe empeorar, como LCP, errores JavaScript o clics de intención de compra; un falso positivo es creer que una mejora es real cuando solo fue ruido estadístico.

El primer prompt para Claude Code debe contener la pregunta de negocio:

Diseña un flujo de A/B testing para un SaaS/blog en Next.js App Router.
El objetivo es monetización, no clics de vanidad.

experiment id: pricing_page_offer_2026_06
hipótesis: cambiar el CTA de pricing de "Start free trial" a "Start with the free plan" aumentará el inicio de registros sin reducir clics de intención de compra.
métrica principal: signup_start_rate
guardrails: purchase_link_click_rate, p75 LCP, JavaScript error rate
entregables: schema de eventos, asignación en servidor, riesgos de Cookie/localStorage, SQL tipo BigQuery, verificación con Playwright y checklist de rollout/rollback.

Usa casos concretos. En SaaS puedes probar el texto del CTA de pricing, el número de pasos de onboarding o el orden de los planes. En un blog puedes probar la posición de un bloque de afiliado, el texto de una newsletter o una caja de consultoría. En un embudo de productos puedes probar si el lead magnet aparece antes o después de la recomendación pagada. Para la base de flags, revisa feature flags con Claude Code; para medición, analítica con Claude Code.

CasoMétrica principalGuardrailsError frecuente
CTA de pricing SaaSInicio de registroClics de compra, errores, LCPMás registros pero menor calidad comercial
Bloque de afiliados en blogClics al productoLectura completa, rebote, velocidadEl bloque aparece demasiado pronto y reduce confianza
Formulario newsletterSuscripciones completadasSpam, bajasLa lista crece pero empeora la calidad
OnboardingPrimer éxitoTickets, activación realLa finalización inicial oculta churn posterior

Congela el schema de eventos antes de tocar la UI

El fallo más caro aparece después del lanzamiento: los datos no se pueden unir. Si el mismo clic se llama button_click, ctaClicked y signup_click, el análisis se convierte en limpieza manual. Pide a Claude Code un contrato de eventos tipado antes de pedir componentes. Si usas Google Analytics, consulta la referencia oficial de eventos GA4 y la referencia de parámetros de Google tag.

// 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,
  });
}

No envíes emails, nombres, empresas ni texto libre en los eventos. Si tu región exige consentimiento para analítica o publicidad, inicializa el estado antes de enviar tags. Google lo documenta en la guía oficial de consent mode.

Asigna variantes en el servidor

localStorage parece cómodo, pero genera parpadeo en el primer render, cambios entre usuario anónimo y login, reinicios en modo privado y problemas cuando el navegador bloquea almacenamiento. MDN describe localStorage como almacenamiento por origen que persiste entre sesiones, pero eso no lo convierte en una buena fuente de verdad para el primer render. Referencia: MDN localStorage.

En Next.js App Router, un Route Handler es un punto de partida práctico. La documentación oficial de route.ts explica cómo crear handlers con Web Request/Response APIs. Para cookies, usa NextResponse. Si necesitas lógica en el borde, ten en cuenta que Next.js 16 renombró Middleware a Proxy; consulta 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;
}

Las cookies también tienen límites. La guía de MDN sobre configuración segura de cookies cubre Secure, HttpOnly y SameSite. En SaaS puedes usar un id de usuario hasheado tras login; en un blog público, una cookie anónima con caducidad corta; en publicidad, el estado del CMP debe mandar.

Separa experimento, rollout y rollback

El código del experimento puede estar desplegado, pero la exposición debe poder cambiar sin redeploy. Vercel ofrece Vercel Flags; para empezar, una configuración explícita también sirve.

# 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"

La regla de rollback evita discusiones cuando el equipo se enamora de la versión nueva. Si suben los errores, empeora LCP o cae la intención de compra, corta exposición y conserva datos para auditoría. Después escala de 10% a 50% y 100%.

Analiza desde la exposición y evita falsos positivos

El denominador debe ser la exposición. Un usuario que nunca vio la variante no pertenece al experimento. Si un usuario vio dos variantes, exclúyelo o investiga. En BigQuery, SAFE_DIVIDE evita fallos por división entre cero.

-- 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;

Define tamaño de muestra y ventana de observación antes del lanzamiento. Mirar el resultado cada día y parar cuando la versión nueva va arriba aumenta falsos positivos. También contaminan el resultado las campañas pagadas simultáneas, demasiados segmentos y cambiar la métrica principal a mitad del experimento.

Verifica con Playwright

Antes de publicar, comprueba que el mismo usuario recibe la misma variante, que un experimento desconocido falla y que el CTA de monetización aparece una sola vez. Playwright documenta test y expect, además de assertions con reintento automático.

// 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);
  });
});

En la práctica de Masa, el mayor avance no fue el SQL, sino escribir primero la tabla de eventos. Decidir qué clic cuenta como CTA de monetización, cuándo registrar exposición y qué hacer con usuarios en dos variantes redujo mucho el retrabajo de Claude Code. En una prueba pequeña, Playwright detectó un parpadeo causado por localStorage, así que la asignación volvió al servidor con cookie.

Para llevar esto a un embudo real, revisa Claude Code training y product templates. El objetivo no es lanzar más experimentos, sino que cada uno responda una pregunta de negocio sin dañar la confianza del usuario.

#Claude Code #A/B testing #SaaS #monetización #Next.js #analítica
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.