Using YARP as a split testing tool
This post assumes you have some knowledge or experience with YARP
There are many ways, and many tools for testing changes to website. These can range from per-page frontend tooling (think Optimizely, or the sunsetting Google Optimize), or perhaps you hard code elements in your backend pages using something like Scientist. Running tests themselves classically is done at the page/URL level, or perhaps a component on the page.
What if you have developed an entirely new website, what are you options for launching? Do you go big bang and hope and pray your user studies are correct and it won't entirely tank your engagement or conversion rate? What happens if Google sees those pages and you take a negative hit in SERPS?
An approach to this challenge could be to release a new version of your website to a specific audience or a specific % of that audience. But to do that at the website level generally means you need to re-think the front-door, and this is where a proxy is a good fit.
Leveraging a proxy, beyond the classical benefits of defining security boundaries, or amalgamating various backend services into a single point of presence, a proxy could also have the capabilities to split traffic between distinct versions of your website.
If you're in the .NET space like me, you have a couple of options - perhaps IIS' Application Request Routing (ARR) feature paired with some server farms, or there is another candidate in the mix, and that is Yet Another Reverse Proxy (YARP).
The Test Scenario
A customer as a legacy website, that has reached the limits of the technology and has invested in a new website, perhaps on a completely different technology stack. In my example, a customer with a legacy .NET website built on a custom CMS, has invested in a new Gatsby-powered statically-built website.
The new website is completely different, the HTML is different, the content, meta tags, usability, etc. have all been changed, refined (hopefully), but the customer naturally has concerns about doing a big-bang changeover.
YARP Configuration
So, before we begin, we need to define two different clusters representing our two websites (note - in our tests, our legacy website is known as baseline
and the new website is known as candidate
):
Now we've defined our two clusters, we need to handle routing. Because this is a complete website split test, we need just a single catch-all route.
It would be possible I believe with some tweaking, to handle this at the route-level, to give your more granularity for running tests against different sections of the website too.
Test Configuration
OK, now we're going to jump into code and start defining some test configuration classes, these will utilize the Options library so we can hydrate these from any configuration source.
These options allow us to configure our test scenario:
Let's talk through these settings so get familiar with they all do:
Test:CookieName
- this represents the name of a cookie used to control the traffic. Having a cookie means we do not need to remember state, we can simply forward the request to the right cluster. This also provides the ability to force a specific version of a test, which is great for debugging.Test:CookieDomain
: The domain property of our generated cookie. We specify this to limit the test to the right host. We don't want our test cookie potentially bleeding out into other subdomains.Test:Baseline:ClusterId
- The ID of the cluster representing our baseline - the legacy website.Test:Baseline:CookieValue
- We usev1
to represent the baseline for the cookie value, and also the query string parameter value (more on that later).Test:Candidate:ClusterId
- Same asTest:Baseline:ClusterId
but for the candidate.Test:Candidate:CookieValue
- Same asTest:Baseline:CookieValue
, but for the candidate, sov2
.Test:Candidate:Weighting
- This is how we bucket between the two versions, expressed as adouble
between0.0
and1.0
. In our example, our intial test is for0.1
or 10% of our traffic.Test:Candidate:AllowBots
- We'll restrict search engine bots (like Googlebot), to the baseline for now.Test:Candidate:AllowChannels
- We'll restrict paid and social channels to the baseline for now too.
Implementing split testing with YARP
Now we've defined everything, let's get going with YARP. The first thing you'll need to do, is add a package reference to your project:
<PackageReference Include="Yarp.ReverseProxy" Version="2.0.0" />
And then let's create our ASP.NET application:
Running this alone will just route all traffic to our baseline, because the default cluster for the {**catch-all}
route is baseline
.
We now need to implement some test code that redirects traffic. Let's define some utility methods first:
These helpers are pretty naïve, particularly around matching bots and channels, but as a starting point it will serve us just fine.
OK, let's build our test proxy! We're going to be taking advantage of some YARP bits that allow us to dynamically switch the cluster as part of our test.
There is a lot to unpack here, so let's break it down:
- We grab our defined options as from the
ApplicationServices
scope, these are singleton so this is fine to hold onto. We're not dynamically reloading these settings in this example. - We use the
IProxyStateLookup
service to get theClusterState
items we need for later reassignment. - We grab both the cookie and parameter values representing out test cases.
- We first check if we have a matching query parameter. E.g. https://www.mydomain.com?website_ver=v2. This will allow us to force an override of the specific version if we need to.
- We then check if we have a matching cookie. This ensures repeat visitors (and subsequent requests for other types of resources, e.g. CSS and JS will get the same version)
- If applicable, we filter out audiences like paid and social, and also bots like Googlebot, if you want to exclude them from the test.
- If we do not have an existing cookie value, or parameter override, we need to split the results. We specifically checkif the weighting is 0%, which will always result in the baseline, or 100% which will always result in the candidate. If the weighting is less than
1.0
, we useRandom.Shared.NextDouble()
to return a random value representing our placement in the test. - We then set a cookie if we need to (we skip this for channels, because if you later allow this and they had the cookie they'd never see the new version). We make sure the cookie is
HttpOnly = false
We do this because the cookie itself can then be reported through Google Analytics for segmenting and analysis. - Lastly, we use
context.ReassignProxyRequest
to tell YARP to use which ever cluster we need for testing.
Over the course of time, when you have confidence in your test candidate. You can incrementally increase traffic by adjusting the weighting.