Derek Johnson
Derek Johnson

Reputation: 322

How to do A/B testing with Cloudflare workers

I'm looking for an example of what these two lines of code look like in a functioning A/B testing worker. From https://developers.cloudflare.com/workers/examples/ab-testing

const TEST_RESPONSE = new Response("Test group") // e.g. await fetch("/test/sompath", request)
const CONTROL_RESPONSE = new Response("Control group") // e.g. await fetch("/control/sompath", request)

I used the examples, subsituting the paths I’m using, and got a syntax error saying await can only be used in async. So I changed the function to async function handleRequest(request) and got a 500 error.

What should these two lines look like for the code to work?

Upvotes: 2

Views: 1482

Answers (2)

nkcmr
nkcmr

Reputation: 11000

Okay, I've worked on this a bit and this worker below seems to fulfill your requirements:

async function handleRequest(request) {
  const NAME = "experiment-0";
  try {
    // The Responses below are placeholders. You can set up a custom path for each test (e.g. /control/somepath ).
    const TEST_RESPONSE = await fetch(
      "https://httpbin.org/image/jpeg",
      request
    );
    const CONTROL_RESPONSE = await fetch(
      "https://httpbin.org/image/png",
      request
    );

    // Determine which group this requester is in.
    const cookie = request.headers.get("cookie");
    if (cookie && cookie.includes(`${NAME}=control`)) {
      return CONTROL_RESPONSE;
    } else if (cookie && cookie.includes(`${NAME}=test`)) {
      return TEST_RESPONSE;
    } else {
      // If there is no cookie, this is a new client. Choose a group and set the cookie.
      const group = Math.random() < 0.5 ? "test" : "control"; // 50/50 split
      const response = group === "control" ? CONTROL_RESPONSE : TEST_RESPONSE;
      return new Response(response.body, {
        status: response.status,
        headers: {
          ...response.headers,
          "Set-Cookie": `${NAME}=${group}; path=/`,
        },
      });
    }
  } catch (e) {
    var response = [`internal server error: ${e.message}`];
    if (e.stack) {
      response.push(e.stack);
    }
    return new Response(response.join("\n"), {
      status: 500,
      headers: {
        "Content-type": "text/plain",
      },
    });
  }
}

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

There are some issues with the snippet once you uncomment the fetch() parts:

  • Yes, the function needs to be async in order to use await keyword, so I added that.
  • Additionally, I think the 500 error you were seeing was also due to a bug in the snippet: You'll notice I form a new Response instead of modifying the one that was chosen from the ternary expression. This is because responses are immutable in the workers runtime, so adding headers to an already instantiated response will result in an error. Therefore, you can just create an entirely new Response with all the bits from the original one and it seems to work.
  • Also, in order to gain insight into errors in a worker, always a good idea to add the try/catch and render those errors in the worker response.

To get yours working on this, just replace the httpbin.org urls with whatever you need to A/B test.

Upvotes: 1

Vulwsztyn
Vulwsztyn

Reputation: 2261

Working example

document.getElementById("myBtn").addEventListener("click", myFun);



async function myFun() {
  const isTest = Math.random() > 0.5 //here you place your condition
  const res = isTest ? await fetch1() : await fetch1() //here you should have different fetches for A and B
  console.log(res)
  document.getElementById("demo").innerHTML = res.message;
}

async function fetch1() {
  //I took the link from some other SO answer as I was running into CORS problems with e.g. google.com
  return fetch("https://currency-converter5.p.rapidapi.com/currency/list?format=json", {
      "method": "GET",
      "headers": {
        "x-rapidapi-host": "currency-converter5.p.rapidapi.com",
        "x-rapidapi-key": "**redacted**"
      }
    })
    .then(response => response.json())
}
<button id="myBtn">button</button>
<div id="demo">demo</div>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script type="text/babel">

</script>

Upvotes: 1

Related Questions