A/B Testing with Server Push Serves Wrong Assets


#1

It seems that when Cloudflare makes the requests to start server push (from Link headers) it does not include the a/b test cookie. As a result the page has no guarantee of getting the correct assets pushed to it.

Our use case:

We are working on an PWA, built with Preact, that is completely served out of the worker script (including static assets). This leads to amazing performance: https://github.com/DigitalOptimizationGroup/cloudflare-worker-preact-pwa

We’d like to be able to run a/b tests and canary release new versions of our apps built with this architecture. And we also just released an edge proxy that enables these features, so the functionality is totally working: https://github.com/DigitalOptimizationGroup/cloudflare-edge-proxy

But… we ran into one issue here. If our apps use server push (and we’d like to) they don’t get to see the cookie and we are left with weird conditionals in the code to make sure the right assets come from the right backend.

We’ve put together a contrived example below that can completely run in a single worker. Our use case is considerably more complex, but this illustrates the issue we are facing.

If you run this script with const ENABLE_SERVER_PUSH = true; then you will notice that we don’t always get the proper css pushed to the app. If you disable server push const ENABLE_SERVER_PUSH = false; then everything works as expected.

Any chance of there being a solution for this?

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

const ENABLE_SERVER_PUSH = true;

async function fetchAndApply(request) {
    const { pathname } = new URL(request.url);

    // here we get or set the version "a" or "b"
    const cookie = request.headers.get("Cookie");
    const version =
        cookie && cookie.includes(`test=a`)
            ? "a"
            : cookie && cookie.includes(`test=b`)
                ? "b"
                : Math.random() < 0.5
                    ? "a"
                    : "b";

    // we always set the test version cookie, just for simplicity
    let headers = new Headers({
        "Content-Type": "text/html",
        "Set-Cookie": `test=${version}`
    });

    if (version === "a") {
        // here we push the three stylesheets that will be needed by this version
        if (ENABLE_SERVER_PUSH) {
            headers.append(
                "Link",
                ["/h1.css", "/h2.css", "/h3.css"]
                    .map(resource => `<${resource}>; rel=preload; as=style`)
                    .join(", ")
            );
        }

        const router = {
            "/": `
                <html>
                    <head>
                        <link href="/h1.css" rel="stylesheet">
                        <link href="/h2.css" rel="stylesheet">
                        <link href="/h3.css" rel="stylesheet">
                    </head>
                    <body>
                        <h1>Hello world - A (blue)</h1>
                        <h2>Hello world - A (blue)</h2>
                        <h3>Hello world - A (blue)</h3>
                    </body>
                </html>`,
            "/h1.css": "h1{color: blue;}",
            "/h2.css": "h2{color: blue;}",
            "/h3.css": "h3{color: blue;}"
        };

        return new Response(router[pathname], {
            status: 200,
            headers
        });
    }

    if (version === "b") {
        if (ENABLE_SERVER_PUSH) {
            headers.append(
                "Link",
                ["/h1.css", "/h2.css", "/h3.css", "/h4.css"]
                    .map(resource => `<${resource}>; rel=preload; as=style`)
                    .join(", ")
            );
        }

        const router = {
            "/": `
                <html>
                    <head>
                        <link href="/h1.css" rel="stylesheet">
                        <link href="/h2.css" rel="stylesheet">
                        <link href="/h3.css" rel="stylesheet">
                        <link href="/h4.css" rel="stylesheet">
                    </head>
                    <body>
                        <h1>Hello world - B (red)</h1>
                        <h2>Hello world - B (red)</h2>
                        <h3>Hello world - B (red)</h3>
                        <h4>Hello world - B (red)</h4>
                    </body>
                </html>`,
            "/h1.css": "h1{color: red;}",
            "/h2.css": "h2{color: red;}",
            "/h3.css": "h3{color: red;}",
            "/h4.css": "h4{color: red;}"
        };

        return new Response(router[pathname], {
            status: 200,
            headers
        });
    }
}