Login Contact Us
My cart

How to make Varnish serve different page versions based on the user's cookie in Magento 2

Apr 1, 2019
by Mexbs Team
Magento Tutorials

In this article, I will demonstrate two different solutions to how to make Varnish serve the page versions based on the user’s cookie.

Magento 2 & Varnish

Magento 2 integrates with Varnish cache out of the box. When Varnish is turned on, every GET request to CMS / Product / Category pages is getting cached by Varnish. Next time a user requests the page, the cached version will be returned by Varnish, instead of fetching the page from Magento.

The challenge of fetching the pages from Varnish based on user’s cookie

Sometimes, developers need to display different versions of the same page, based on a cookie. The best example for that is A/B testing. In A/B testing, the user comes to the website, with the version cookie set for him. The website then displays the page according to the version the user is assigned to.

So the challenge is to be able to display different page versions to the user, based on his cookie. There are two solutions to this problem and I will show them to you in this tutorial.

First, let’s see the task that I am trying to achieve, and then I will show you two solutions for it.

The task: A/B testing the CMS pages, based on user’s cookie

In this task I created a custom header block that I inject before the content block.

In my custom block, I display a message, based on the user’s cookie. I will display the text “Hello Version A user!” above the content block in a CMS page, to the user who has the cookie mexbs_page_version with value version_a or no mexbs_page_version cookie at all. A user who has the cookie mexbs_page_version with value version_b will see the text “Hello Version B user!”.

Here is my current module structure, it is fairly simple:

Here is the content of Mexbs/VarnishCookie/view/frontend/layout/cms_page_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceContainer name="content">
        <block class="Mexbs\VarnishCookie\Block\CustomHeader" before="-" name="mexbs_custom_header" template="Mexbs_VarnishCookie::mexbs_custom_header.phtml" />
    </referenceContainer>
</page>

Here is the content of Mexbs/VarnishCookie/view/frontend/templates/mexbs_custom_header.phtml

<?php
/**
 * @var \Mexbs\VarnishCookie\Block\CustomHeader $block
 */
?>
<?php echo $block->getHelloText(); ?>

Here is the content of Mexbs/VarnishCookie/Block/CustomHeader.php

<?php
namespace Mexbs\VarnishCookie\Block;

class CustomHeader extends \Magento\Framework\View\Element\Template
{
    protected  $cookieManager;

    public function __construct(
        \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager,
        \Magento\Framework\View\Element\Template\Context $context,
        array $data = []
    )
    {
        $this->cookieManager = $cookieManager;
        parent::__construct($context, $data);
    }

    public function getHelloText(){
        $versionCookieValue = $this->cookieManager->getCookie(\Mexbs\VarnishCookie\Helper\Data::PAGE_VERSION_COOKIE_NAME);
        if($versionCookieValue == \Mexbs\VarnishCookie\Helper\Data::VERSION_A_COOKIE_VALUE){
            return "Hello Version A user!";
        }elseif($versionCookieValue == \Mexbs\VarnishCookie\Helper\Data::VERSION_B_COOKIE_VALUE){
            return "Hello Version B user!";
        }else{
            return "Hello Version A user!";
        }
    }
}

Here is the content of Mexbs/VarnishCookie/Helper/Data.php

<?php
namespace Mexbs\VarnishCookie\Helper;

class Data
{
    const PAGE_VERSION_COOKIE_NAME = 'mexbs_page_version';
    const VERSION_A_COOKIE_VALUE = 'version_a';
    const VERSION_B_COOKIE_VALUE = 'version_b';
}

The problem: Varnish serves the first version that was browsed by any user

As expected, on the first visit of any user - the version of the page is getting cached. So any subsequent user gets this version. For example, if user A has 'version_a' cookie, and he goes to the home page, he will see the message “Hello Version A user!”. But if another user with 'version_b' cookie will go to the home page, he will also see the message “Hello Version B user!”.

Solution #1: Add the Cookie to the Varnish VCL file

The best solution for the problem is to add our cookie to the VCL file. So Varnish will create a separate version for every page with the different cookie value. Magento already does it for the X-Magento-Vary cookie.

So all we need to do is to paste the following code (marked in yellow) to our VCL file. (It is typically located at <your_magento_root_directory>/var/varnish.vcl, in my example it is located at /var/www/magento-218//var/varnish.vcl.)
Note: In this example, I use Varnish 4.

sub vcl_hash {
    if (req.http.cookie ~ "X-Magento-Vary=") {
        hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1"));
    }
    
    if (req.http.cookie ~ "mexbs_page_version=") {
        hash_data(regsub(req.http.cookie, "^.*?mexbs_page_version=([^;]+);*.*$", "\1"));
   }


    # For multi site configurations to not cache each other's content
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }

    # To make sure http users don't see ssl warning
    if (req.http./* {{ ssl_offloaded_header }} */) {
        hash_data(req.http./* {{ ssl_offloaded_header }} */);
    }
    /* {{ design_exceptions_code }} */
}

Solution #2: Use the X-Magento-Vary cookie

This solution is worse than Solution #1 because here Varnish will serve the wrong page on the first request. However, it will serve the correct pages on the subsequent requests. The benefit of this solution, however, is that you don’t touch the VCL file. Therefore, this solution can be useful if you are developing an extension or a module where you can’t touch the customer’s VCL.

How does it work?

A user with cookie mexbs_page_version cookie “version_a” browses to some CMS page. Let’s assume for the sake of the example that he browses to the home page. He sees the message “Hello Version A user!”. Another user with cookie mexbs_page_version “version_b” browses to the home page. Since Varnish has cached the page already, and the user’s X-Magento-Vary cookie is not set, this user will see “Hello Version A user!”. Note that if user B had the X-Magento-Vary cookie set with the hash of versin_b set in it, Varnish would load the proper page.

However, as soon as the second user’s page loads, our module will send an AJAX POST request to the server, which will cause the X-Magento-Vary cookie to refresh. That is since the POST is not cached, the request will go to Magento, and in Magento, our module will update the X-Magento-Vary cookie to the proper one. Therefore, the next time this user browses to any CMS page, he will see the correct message “Hello Version B user!”.

Here is a diagram to make it clearer:

The code

The code of solution #2 is a bit more sophisticated. In addition to our current files, we added some more files that help us to make the Ajax POST request and to update the X-Magento-Vary cookie. Here is our new file structure:

The refresh-vary-cookie.phtml file makes the Ajax POST call to Magento. The Donothing.php controller implements the call. As its name suggests - it doesn’t do anything. However, when this call occurs, the plugin FrontController.php updates the value of the X-Magento-Vary cookie, by updating the context object, according to the mexbs_page_version cookie. Later on, Magento generates the X-Magento-Vary cookie from the context object.

Now I will show the code of the new files. You can also download the full module here.

Here is the content of Mexbs/VarnishCookie/view/frontend/templates/refresh-vary-cookie.phtml

<?php
/**
 * @var \Magento\Framework\View\Element\Template $block
 */
?>
<script>
    require([
        "jquery"
    ], function($){
        $.post("<?php echo $block->getUrl('mbvcookie/action/donothing', ['_secure' => $block->getRequest()->isSecure()]) ?>", function(data) {});
    });
</script>

Here is the content of Mexbs/VarnishCookie/view/frontend/layout/cms_page_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceContainer name="content">
        <block class="Mexbs\VarnishCookie\Block\CustomHeader" before="-" name="mexbs_custom_header" template="Mexbs_VarnishCookie::mexbs_custom_header.phtml" />
    </referenceContainer>
    <referenceContainer name="before.body.end">
        <block class="Magento\Framework\View\Element\Template"
               name="nexbs.refresh.vary.cookie"
               template="Mexbs_VarnishCookie::refresh-vary-cookie.phtml"
                />
    </referenceContainer>
</page>

Here is the content of Mexbs/VarnishCookie/Controller/Action/Donothing.php

<?php
namespace Mexbs\VarnishCookie\Controller\Action;

class Donothing extends \Magento\Framework\App\Action\Action
{
    private $resultJsonFactory;

    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
    ){
        $this->resultJsonFactory = $resultJsonFactory;
        parent::__construct($context);
    }

    public function execute()
    {
        return $this->resultJsonFactory->create()->setData(['success' => 'true']);
    }
}

Here is the content of Mexbs/VarnishCookie/etc/frontend/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
    <router id="standard">
        <route id="mbvcookie" frontName="mbvcookie">
            <module name="Mexbs_VarnishCookie" />
        </route>
    </router>
</config>

Here is the content of Mexbs/VarnishCookie/etc/frontend/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\App\FrontController">
        <plugin name="mexbsAppFrontController" type="Mexbs\VarnishCookie\Plugin\App\FrontController"/>
    </type>
</config>

Here is the content of Mexbs/VarnishCookie/Plugin/App/FrontController.php

<?php
namespace Mexbs\VarnishCookie\Plugin\App;

use Magento\Framework\App\RequestInterface;

class FrontController
{
    private $cookieManager;
    private $httpContext;


    public function __construct(
        \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager,
        \Magento\Framework\App\Http\Context $httpContext
    ) {
        $this->cookieManager = $cookieManager;
        $this->httpContext = $httpContext;
    }

    public function beforeDispatch(\Magento\Framework\App\FrontController $subject, RequestInterface $request){
        $versionCookieValue = $this->cookieManager->getCookie(\Mexbs\VarnishCookie\Helper\Data::PAGE_VERSION_COOKIE_NAME);
        $this->httpContext->setValue(
            'mexbs_pversion',
            (!(in_array($versionCookieValue,
                [\Mexbs\VarnishCookie\Helper\Data::VERSION_A_COOKIE_VALUE, \Mexbs\VarnishCookie\Helper\Data::VERSION_B_COOKIE_VALUE]))
                ? \Mexbs\VarnishCookie\Helper\Data::VERSION_A_COOKIE_VALUE
                : $versionCookieValue),
            \Mexbs\VarnishCookie\Helper\Data::VERSION_A_COOKIE_VALUE
        );
    }
}

Summing up

In this tutorial, we went through two possible solutions to display different Varnish versions of the page depending on the user’s cookie value. While the first solution is better, because it will always show the correct version of the page - it requires a modification in the VCL file.

The second solution avoids touching the VCL. However, it might display the wrong version of the page on the first customer visit.

Whichever solution you’ve chosen, I hope it worked well for you. Please share your experience with me in the comments!