Personalization at Edge

fastly, rust, personalization

When companies with tiered pricing plans display the same pricing page to all their website visitors, there is a good chance they are leaving money on the table by not presenting the right pricing and packaging to their potential buyers.

In this post, we’ll explore using Rust with Fastly’s Compute@Edge to create a personalized website experience. Using Edge Compute to personalize a website has several advantages over the incumbent thinking of website optimization. Players like Optimizely and Google Optimize require adding an async JavaScript snippet and having the visitor suffer from some degree of “page flicker.”

With Compute@Edge we can ditch the JavaScript snippets and have the CDN render the final page with no additional DOM manipulations.

For this example we’ll create three variations of a pricing page:

  • Enterprise: visitors at companies with >100k employees
  • B2B SASS: visitors at companies in “b2b sass” space
  • Default: fallback for all other visitors

The beauty of this approach is each variant has a static URL on the origin server that can be previewed in a web browser without having to impersonate any of the personalization rules.

On the Edge server we’ll call the Clearbit Reveal API to fetch firmographic data for the visitors IP address and use the data to determine which page variation to render.

The personalization rules have a few important elements:

  • path: the page path to personalize
  • variants: the different variations of the page
  • variant.conditions: the variant criteria
  • variant.path: the new path to fetch from origin server

[{
  "path": "/pricing",
  "variants": [{
    "name": "Enterprise",
    "conditions": [{
      "field": "company.metrics.employees",
      "operator": "gte",
      "value": 100000
    }],
    "path": "/pricing/enterprise"
  }, {
    "name": "B2B SaSS",
    "conditions": [{
      "field": "company.tags",
      "operator": "contains",
      "value": "b2b"
    }, {
      "field": "company.tags",
      "operator": "contains",
      "value": "sass"
    }],
    "path": "/pricing/b2b-sass"
  }]
}]

The rules are loaded into the Rust wasm application and executed against with each incoming request to determine which variation of the page to display.

There is a tiny amount of Rust code deployed to the Edge. The code is responsible for executing the rules and augmenting the page path sent to the origin server if a variant matches.

#[fastly::main]
fn main(mut req: Request<Body>) -> Result<impl ResponseExt, Error> {
    let re = RuleEngine::new(RULES_PATH);
    let path = req.uri().path();

    // return early if no variants for requested path
    if !re.has_variant_for_path(path) {
        return Ok(req.send(BACKEND_NAME)?);
    }

    // fetch visitor ip address
    let ip_addr = downstream_client_ip_addr().ok_or(anyhow!("could not get client ip"))?;

    // fetch company firmographic data for IP address
    let company = reveal_lookup(ip_addr);

    // run the rule engine to find personalized variant
    let variant = re.find_variant(path, company);
    let new_path = variant.path;

    // augment the request with the new path
    let mut parts = req.uri().clone().into_parts();
    let path_and_query = match req.uri().query() {
        Some(query) => format!("{}?{}", new_path, query),
        None => String::from(new_path),
    }
    .parse()?;

    parts.path_and_query = Some(path_and_query);
    let uri = fastly::http::Uri::from_parts(parts)?;
    *req.uri_mut() = uri;

    // send request to origin server
    return Ok(req.send(BACKEND_NAME)?);
}

Using the appropriate Cache Control Headers we can serve up subsequent personalized pricing pages without any further requests to the origin server.

All that said, I’m super excited about the future of Edge Compute. Having Rust (or any modern language) available at the Edge feels like a huge leap forward in CDN technology. On top of that deploying with the Fastly toolchain gave me flashbacks to the first time using Heroku.

Edit: Thanks @growthwtf for pointing out my incorrect usage of “It just works” LOL (it actually does work!)