You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Lambda@Edge实现CloudFront A/B测试的功能问题求助

Lambda@Edge + CloudFront A/B测试:第3点需求失效排查与修复

问题概述

使用Lambda@Edge(Viewer Request触发)配合CloudFront实现A/B测试,核心需求:

  • 无ab_test_variant cookie时,随机分配A/B并设置cookie、追加variant查询参数;
  • 有cookie且已带variant参数时,直接放行;
  • 有cookie但无variant参数时,追加与cookie匹配的variant参数。

目前前两点正常,第三点失效,确认Viewer Request触发位置正确,寻求修复方案。

当前代码如下:

const TESTING_SPLIT = 0.5;
const COOKIE_NAME = 'ab_test_variant';

const TestingVariantEnum = {
  VariantA: 'A',
  VariantB: 'B',
};

const getRandomVariant = () =>
  Math.random() > TESTING_SPLIT
    ? TestingVariantEnum.VariantA
    : TestingVariantEnum.VariantB;

const parseCookie = (cookieHeader) => {
  const cookies = cookieHeader.split(';');
  const cookie = cookies.find((cookieValue) =>
    cookieValue.trim().startsWith(COOKIE_NAME)
  );
  if (cookie) {
    const value = cookie.split('=');
    return value[value.length - 1].trim();
  }
  return undefined;
};

const findCookie = (headers) => {
  if (headers.cookie) {
    for (const cookieHeader of headers.cookie) {
      const cookieValue = parseCookie(cookieHeader.value);
      if (cookieValue) {
        return cookieValue;
      }
    }
  }
  return undefined;
};

exports.handler = async (event) => {
  const { request } = event.Records[0].cf;
  const { headers, uri, querystring } = request;

  const currentVariant = findCookie(headers);
  const hasQueryVariant = querystring.includes('variant=');

  if (currentVariant && !hasQueryVariant) {
    const newQuerystring = querystring
      ? `${querystring}&variant=${currentVariant}`
      : `variant=${currentVariant}`;
    request.querystring = newQuerystring;
    return request;
  }

  if (currentVariant && hasQueryVariant) {
    return request;
  }

  const variant = getRandomVariant();
  const newQuerystring = querystring
    ? `${querystring}&variant=${variant}`
    : `variant=${variant}`;

  const response = {
    status: '302',
    statusDescription: 'Found',
    headers: {
      'cache-control': [
        {
          key: 'Cache-Control',
          value: 'no-store',
        },
      ],
      'set-cookie': [
        {
          key: 'Set-Cookie',
          value: `${COOKIE_NAME}=${variant}; Path=/; HttpOnly`,
        },
      ],
      location: [
        {
          key: 'Location',
          value: `${uri}?${newQuerystring}`,
        },
      ],
    },
  };

  return response;
};

问题根源

  1. 查询参数判断逻辑不严谨:querystring.includes('variant=')会误匹配类似myvariant=A的参数,导致实际无variant时被判定为存在;
  2. Cookie匹配逻辑有漏洞:startsWith(COOKIE_NAME)会匹配ab_test_variant_foo这类相似cookie,导致获取错误的variant值;
  3. 缓存策略冲突:若CloudFront缓存键未包含ab_test_variant cookie和variant参数,会导致修改后的请求被缓存,后续请求无法触发Lambda逻辑。

修复方案

1. 修正核心判断逻辑

  • 优化Cookie匹配,确保精准匹配目标cookie;
  • 改用拆分查询参数的方式,精准判断variant是否存在。

2. 调整CloudFront缓存策略

在CloudFront分发的缓存行为中,配置:

  • 缓存键包含Cookie:指定仅包含ab_test_variant;
  • 缓存键包含Query String:指定仅包含variant;
  • 确保携带cookie的请求不会被错误缓存,保证每个variant的请求都能触发Lambda逻辑。

3. 修正后的完整代码

const TESTING_SPLIT = 0.5;
const COOKIE_NAME = 'ab_test_variant';

const TestingVariantEnum = {
  VariantA: 'A',
  VariantB: 'B',
};

const getRandomVariant = () =>
  Math.random() > TESTING_SPLIT
    ? TestingVariantEnum.VariantA
    : TestingVariantEnum.VariantB;

const parseCookie = (cookieHeader) => {
  const cookies = cookieHeader.split(';');
  // 精准匹配cookie名称,避免相似名称干扰
  const cookie = cookies.find((cookieValue) =>
    cookieValue.trim().startsWith(`${COOKIE_NAME}=`)
  );
  if (cookie) {
    const value = cookie.split('=');
    return value[value.length - 1].trim();
  }
  return undefined;
};

const findCookie = (headers) => {
  if (headers.cookie) {
    for (const cookieHeader of headers.cookie) {
      const cookieValue = parseCookie(cookieHeader.value);
      if (cookieValue) {
        return cookieValue;
      }
    }
  }
  return undefined;
};

exports.handler = async (event) => {
  const { request } = event.Records[0].cf;
  const { headers, uri, querystring } = request;

  const currentVariant = findCookie(headers);
  // 拆分查询参数,精准判断variant是否存在
  const hasQueryVariant = querystring 
    ? querystring.split('&').some(param => param.trim().startsWith('variant=')) 
    : false;

  if (currentVariant && !hasQueryVariant) {
    const newQuerystring = querystring
      ? `${querystring}&variant=${currentVariant}`
      : `variant=${currentVariant}`;
    request.querystring = newQuerystring;
    // 强制不缓存修改后的请求,避免缓存污染
    request.headers['cache-control'] = [{
      key: 'Cache-Control',
      value: 'no-cache, no-store'
    }];
    return request;
  }

  if (currentVariant && hasQueryVariant) {
    return request;
  }

  // 无cookie的逻辑保持不变,建议添加Secure属性(HTTPS环境下)
  const variant = getRandomVariant();
  const newQuerystring = querystring
    ? `${querystring}&variant=${variant}`
    : `variant=${variant}`;

  const response = {
    status: '302',
    statusDescription: 'Found',
    headers: {
      'cache-control': [
        {
          key: 'Cache-Control',
          value: 'no-store',
        },
      ],
      'set-cookie': [
        {
          key: 'Set-Cookie',
          value: `${COOKIE_NAME}=${variant}; Path=/; HttpOnly; Secure`,
        },
      ],
      location: [
        {
          key: 'Location',
          value: `${uri}?${newQuerystring}`,
        },
      ],
    },
  };

  return response;
};

4. 验证步骤

  1. 部署修正后的Lambda@Edge函数到Viewer Request触发位置;
  2. 手动清除CloudFront现有缓存;
  3. 使用浏览器开发者工具设置ab_test_variant=A cookie,访问不带variant参数的URL,检查请求是否自动追加?variant=A;
  4. 查看CloudFront访问日志,确认Lambda函数被触发且querystring修改生效。

内容的提问来源于stack exchange,提问作者Spakainas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 01:00:56