Tips & Tricks (更新: 2026/6/2)

用 Claude Code 做 A/B 测试:面向 SaaS 与博客变现的实战指南

用 Claude Code 设计假设、事件、服务端分流、SQL 分析、隐私同意与回滚,安全优化 SaaS 和博客变现。

用 Claude Code 做 A/B 测试:面向 SaaS 与博客变现的实战指南

先写假设,再写代码

A/B 测试不是把页面随机切成两个版本那么简单。对 SaaS 来说,它要回答免费注册、付费意向、试用转化是否真的改善;对博客来说,它要回答广告、联盟链接、邮件订阅和咨询入口是否带来长期收益,而不是只多了几个点击。Claude Code 很擅长快速生成切换逻辑,但如果没有假设、事件口径、样本量、护栏指标和回滚规则,最后看到的数字很可能不能用于决策。

先把术语说清楚。Variant 是“被比较的版本”。Exposure 是“用户第一次看到某个版本,并被记录下来”。Guardrail metric 是“不能变差的安全指标”,例如 LCP、错误率、付费链接点击率。False positive 是“随机波动看起来像胜利”,常见原因是样本太小、每天偷看结果、同时比较太多版本。

给 Claude Code 的第一条提示词应该从业务问题开始:

为 Next.js App Router 的 SaaS/博客做 A/B 测试。
目标是变现,不是虚荣点击。

实验 ID: pricing_page_offer_2026_06
假设: 把价格页 CTA 从 "Start free trial" 改为 "Start with the free plan",会提高注册开始率,同时不降低付费意向点击。
主要指标: signup_start_rate
护栏指标: purchase_link_click_rate, p75 LCP, JavaScript error rate
请输出: 事件 schema、服务端分流、Cookie/localStorage 注意事项、BigQuery 风格 SQL、Playwright 验证、发布和回滚清单。

实际使用时至少准备三个场景:SaaS 价格页 CTA、博客联盟链接位置、newsletter 注册表单、咨询预约入口、onboarding 步骤。每个场景都要同时写主要指标和护栏指标。功能开关的基础可以参考Claude Code 功能标志实现,埋点体系可以参考Claude Code 分析实现

场景主要指标护栏指标常见失败
SaaS 价格页 CTA注册开始率付费意向点击、错误率、LCP注册多了,但真正愿意付费的人少了
博客广告/联盟区块商品链接点击率阅读完成率、跳出率、速度收益区块太靠前,破坏信任
邮件订阅表单订阅完成率垃圾注册率、退订率数量上升但名单质量下降
Onboarding 页面首次成功率支持请求、激活质量短期完成率掩盖后续流失

先固定事件 Schema

A/B 测试最贵的错误,是上线后才发现数据无法汇总。一个点击如果有时叫 button_click,有时叫 ctaClicked,有时叫 signup_click,分析阶段就会变成人工清洗。让 Claude Code 先输出类型化事件契约,再写 UI。

如果使用 Google Analytics,请先看官方的 GA4 事件参考Google tag 参数参考。自定义参数想在报表中使用,通常还要在 GA4 中配置自定义维度或指标。

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

不要把邮箱、姓名、公司名或用户输入原文放进事件。涉及广告或分析存储时,应在发送 tag 之前处理同意状态。Google 的同意模式指南说明了默认同意状态和更新方式。对变现实验来说,同意不是上线后的补丁,而是实验设计的一部分。

用 Next.js Route Handler 做服务端分流

只用 localStorage 分流很方便,但会带来首屏闪烁、登录前后版本变化、隐身模式清空、浏览器阻止存储、机器人流量不稳定等问题。MDN 把 localStorage 定义为按 origin 保存且可跨浏览器会话保留的数据,参考 MDN localStorage。这不等于它适合做首屏版本来源。

在 Next.js App Router 中,可以先用 Route Handler 实现服务端分配。官方 route.ts 文档说明 Route Handler 使用 Web Request/Response API 处理请求。Cookie 写入可以使用 NextResponse。如果需要在边缘层改写请求,还要注意 Next.js 16 已将 Middleware 改名为 Proxy,参考 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;
}

Cookie 也不是万能答案。MDN 的安全 Cookie 配置指南解释了 SecureHttpOnlySameSite。会员 SaaS 可以在登录后使用哈希化用户 ID;公开博客更适合短期匿名 Cookie;涉及广告时要以 CMP 和地区规则为准。

用功能开关控制发布和回滚

实验和发布要分开。代码可以先上线,但流量比例必须可以从 0%、10%、50% 到 100% 调整。Vercel 用户可以评估官方 Vercel Flags,自建项目则可以先用配置文件。

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

回滚条件要在实验开始前写好。新版本如果让错误率升高、LCP 变慢、付费意向下降,就先把流量设为 0,并保留曝光记录用于复盘。不要看到短期好结果就直接推到 100%。

用 SQL 分析,但不要被假阳性骗到

分析要从 exposure 开始。没有看过实验版本的用户不应进入分母;同一用户看过多个 variant 时,应排除或调查。下面的查询用于汇总转化和护栏,不是自动判断赢家的统计引擎。BigQuery 官方的 SAFE_DIVIDE 文档可以避免除以零导致报表失败。

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

样本量要在实验前确定。每天偷看结果,看到新版本领先就停止,会显著增加假阳性。多版本、多分段、事后更换主要指标、和广告投放同时开始,也会让结论失真。让 Claude Code 输出“最小样本、观察周期、排除规则、停止条件、复盘日期”,比只让它算一个 p 值更可靠。

用 Playwright 验证机制

上线前至少确认三件事:同一匿名 ID 分到同一版本;未知实验 ID 返回错误;变现 CTA 只渲染一次。Playwright 官方文档介绍了 testexpect,以及会自动重试的断言

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

这个测试不证明收入增长,只证明实验基础没有坏。基础坏了,后面的统计分析就没有意义。

隐私、运营和实际结果

变现实验会处理用户行为数据,所以规则要写清楚:不发送个人信息;尊重分析和广告同意;Cookie 被拒绝时有默认版本;记录负责人、开始日期、结束日期、假设、指标、护栏和回滚步骤;失败实验也要留下结论,避免下次重复踩坑。

Masa 把这套流程用于文章 CTA 检查时,最有价值的不是漂亮的报表,而是上线前的事件表。先决定哪些点击算变现 CTA、什么时候记录曝光、同一读者进入多个版本怎么办,Claude Code 的返工明显减少。一次小型测试中,Playwright 的同一 ID 检查发现了 localStorage 首屏闪烁,于是改回服务端 Cookie 分流。

如果要继续把 Claude Code 用到真实收入漏斗,可以看Claude Code 培训产品模板。重点不是制造更多实验,而是让每个实验都能安全回答一个业务问题。

#Claude Code #A/B 测试 #SaaS #博客变现 #Next.js #数据分析
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。