Improving the cache performance of The Polyfill Service even more

From January 13th to January 19th 2019, polyfill.io served 1,672,663,851 requests. 99.994% of which were served from Fastly’s cache. To achieve such a high cache-hit ratio, we employed some novel solutions using VCL. In this article I will explain how we went about achieving this result.

What is polyfill.io and how does it work?

Polyfill.io is a service which serves polyfills for features which are missing in the requesting User-Agent. It works in three broad steps:

  1. It reads the request url to figure out which features the website is wanting to polyfill
  2. It reads the User-Agent header to see what features it is missing
  3. It serves polyfills for the missing features that were included in the request url

How the caching works

Polyfill.io uses Varnish Cache, specifically it uses Fastly’s Varnish Cache. When a request is made to polyfill.io, the Varnish Cache server will handle the request, create a hash key and check if an object in its cache has the corresponding hash key. If it does, then polyfill.io responds with the cached object.

Varnish Cache is a programmable cache, which is great because it allows us to define how the hash key is created. We decided to use the URL path and query-parameters as the hash key because they are the interface to our API. The rest of this post goes into how we made different request URLs end up being the same URL before Varnish Cache generates the hash key.

The code used to generate the hash key
sub vcl_hash {
  if (req.http.Fastly-Debug) {
    call breadcrumb_hash;
  }

  # We are not adding req.http.host to the hash because we want https://cdn.polyfill.io and https://polyfill.io to be a single object in the cache.
  # set req.hash += req.http.host;
  set req.hash += req.url;
  # We include return(hash) to stop the function falling through to the default VCL built into varnish, which for vcl_hash will add req.url and req.http.Host to the hash.
  return(hash);
}

Normalising the query parameters

Polyfill.io users specify what features they need in their request’s query parameters. For example this request:

https://polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded

is configuring the polyfill bundle to contain polyfills for fetch, IntersectionObserver, and to call polyfillsLoaded once done.

Due to flexibility in the way urls can be formulated, many other string formulations would configure this exact same polyfill bundle.
A non-exhaustive list:

  1. polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded
  2. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  3. polyfill.io/v3/polyfill.js?features=fetch,IntersectionObserver&callback=polyfillsLoaded
  4. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=fetch,IntersectionObserver
  5. polyfill.io/v3/polyfill.js?features=fetch,IntersectionObserver&callback=polyfillsLoaded&zebra=striped
  6. polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded&unknown=polyfill

We want all of these different URLs to point to the same response in the cache. To do this they need to use the same hash key. The way we achieve that is to convert them all to the same URL before the Varnish Cache creates the key.

Some of the URLs in the list have the exact same query parameters, only in different orders. We can re-order the query parameters with a function that Fastly provide called querystring.sort. This function alone will turn URLs 1 and 2 into the same URL. It will do the same for URLs 3 and 4.

sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  set req.url = querystring.sort(req.url);

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

Listing the URLs again, now with the query parameters sorted:

  1. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  2. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  3. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=fetch,IntersectionObserver
  4. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=fetch,IntersectionObserver
  5. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=fetch,IntersectionObserver&zebra=striped
  6. polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded&unknown=polyfill

There are still more differences that we can normalise:

The order of the comma-separated features in the features parameter isn’t the same for URLs 2 and 3. We would need to sort those features by some manner in order to make them identical. Neither Varnish Cache nor Fastly offer a pre-built function to sort a string, VCL also does not have a way to loop through items either, which makes this a bit trickier to solve.

The way we solved this issue was by creating a function which takes a comma-separated string and turns it into a URL where each item in the comma-separated string is a lone-standing query parameter. Then we can use the same querystring.sort function that we used earlier, and finally, we turn the query parameters back into a comma-separated string.

sub sort_comma_separated_value {
  # This function takes a CSV and tranforms it into a url where each
  # comma-separated-value is a query-string parameter and then uses
  # Fastly's querystring.sort function to sort the values. Once sorted
  # it then turn the query-parameters back into a CSV.
  # Set the CSV on the header `Sort-Value`.
  # Returns the sorted CSV on the header `Sorted-Value`.
  declare local var.value STRING;
  set var.value = req.http.Sort-value;

  # If query value does not exist or is empty, set it to ""
  set var.value = if(var.value != "", var.value, "");

  # Replace all `&` characters with `^`, this is because `&` would break the value up into pieces.
  set var.value = regsuball(var.value, "&", "^");

  # Replace all `,` characters with `&` to break them into individual query values
  # Append `1-` infront of all the query values to make them simpler to transform later
  set var.value = "1-" regsuball(var.value, ",", "&1-");

  # Create a url-like string in order for querystring.sort to work.
  set var.value = querystring.sort("https://www.example.com?" var.value);

  # Grab all the query values from the sorted url
  set var.value = regsub(var.value, "https://www.example.com\?", "");

  # Reverse all the previous transformations to get back the single `features` query value value
  set var.value = regsuball(var.value, "1-", "");
  set var.value = regsuball(var.value, "&", ",");
  set var.value = regsuball(var.value, "\^", "&");

  set req.http.Sorted-Value = var.value;
}

sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  if (req.url.qs ~ "(?i)[^&=]*features=([^&]+)") {
    # Need to decode %2C into ,
    set req.http.Sort-Value = urldecode(re.group.1);
    call sort_comma_separated_value;
    set req.url = querystring.set(req.url, "features", req.http.Sorted-Value);
  }

  set req.url = querystring.sort(req.url);

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

Using this function will make URLs 1, 2, 3 and 4 identical.

Listing the URLs again, after this function has been used:

  1. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  2. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  3. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  4. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  5. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch&zebra=striped
  6. polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded&unknown=polyfill

The 5th URL has configured zebra to striped, but zebra is not part of the API for configuring a polyfill bundle. Let’s add a function to only keep query parameters in the URL which are actually part of the public API. We can achieve this with a function that Fastly provide called querystring.regfilter_except. Using this function will make URLs 1, 2, 3, 4, and 5 become identical.

sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  # Remove all querystring parameters which are not part of the public API.
  set req.url = querystring.regfilter_except(req.url, "^(features|excludes|rum|unknown|flags|version|ua|callback|compression)$");

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

Listing the URLs again, after this function has been used:

  1. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  2. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  3. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  4. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  5. polyfill.io/v3/polyfill.js?callback=polyfillsLoaded&features=IntersectionObserver,fetch
  6. polyfill.io/v3/polyfill.js?features=IntersectionObserver,fetch&callback=polyfillsLoaded&unknown=polyfill

The 6th URL has set unknown to polyfill, which just happens to be the same as the default value for unknown. If we add the default values into the URL for the parameters which have not been set, we can make all the URLs in the list become identical. Remember, we use the URL as the hash key within Fastly, so making these requests use the same URL internally will mean that they point to the same response in the cache, which is what we are trying to achieve.

sub normalise_querystring_parameters_for_polyfill_bundle {
  # Remove all querystring parameters which are not part of the public API.
  set req.url = querystring.regfilter_except(req.url, "^(features|excludes|rum|unknown|flags|version|ua|callback|compression)$");

  # (?i) makes the regex case-insensitive
  # The regex will match only if their are characters after `features=` which are not an ampersand (&).
  if (req.url.qs ~ "(?i)[^&=]*features=([^&]+)") {
    # Parameter has already been set, use the already set value.
    # re.group.1 is the first regex capture group in the regex above.
    if (std.strlen(re.group.1) < 100) {
      # We add the value of the features parameter to this header
      # This is to be able to have sort_comma_separated_value sort the value
      set req.http.Sort-Value = urldecode(re.group.1);
      call sort_comma_separated_value;
      # The header Sorted-Parameter now contains the sorted version of the features parameter.
      set req.url = querystring.set(req.url, "features", req.http.Sorted-Value);
    }
  } else {
    # Parameter has not been set, use the default value.
    set req.url = querystring.set(req.url, "features", "default");
  }

  # (?i) makes the regex case-insensitive
  # The regex will match only if their are characters after `excludes=` which are not an ampersand (&).
  if (req.url.qs ~ "(?i)[^&=]*excludes=([^&]+)") {
    # Parameter has already been set, use the already set value.
    # re.group.1 is the first regex capture group in the regex above.
    if (std.strlen(re.group.1) < 100) {
      # We add the value of the excludes parameter to this header
      # This is to be able to have sort_comma_separated_value sort the value
      set req.http.Sort-Value = urldecode(re.group.1);
      call sort_comma_separated_value;
      # The header Sorted-Parameter now contains the sorted version of the excludes parameter.
      set req.url = querystring.set(req.url, "excludes", req.http.Sorted-Value);
    }
  } else {
    # If excludes is not set, set to default value ""
    set req.url = querystring.filter(req.url, "excludes");
    set req.url = if(req.url ~ "\?", req.url "&excludes=", req.url "?excludes=");
  }

  # If rum is not set, set to default value "0"
  if (req.url.qs !~ "(?i)[^&=]*rum=([^&]+)") {
    set req.url = querystring.set(req.url, "rum", "0");
  }

  # If unknown is not set, set to default value "polyfill"
  if (req.url.qs !~ "(?i)[^&=]*unknown=([^&]+)") {
    set req.url = querystring.set(req.url, "unknown", "polyfill");
  }

  # If flags is not set, set to default value ""
  if (req.url.qs !~ "(?i)[^&=]*flags=([^&]+)") {
    set req.url = querystring.filter(req.url, "flags");
    set req.url = if(req.url ~ "\?", req.url "&flags=", req.url "?flags=");
  }

  # If version is not set, set to default value ""
  declare local var.version STRING;
  if (req.url.qs !~ "(?i)[^&=]*version=([^&]+)") {
    set req.url = querystring.filter(req.url, "version");
    set req.url = if(req.url ~ "\?", req.url "&version=", req.url "?version=");
  }

  # If ua is not set, normalise the User-Agent header based upon the version of the polyfill-library that has been requested.
  if (req.url.qs !~ "(?i)[^&=]*ua=([^&]+)") {
    if (req.url.qs ~ "(?i)[^&=]*version=3\.25\.1(&|$)") {
      # normalise_user_agent function is too large for Fastly Fiddle.
      # call normalise_user_agent;
    } else {
      # normalise_user_agent_latest function is too large for Fastly Fiddle.
      # call normalise_user_agent_latest;
    }
    set req.url = querystring.set(req.url, "ua", req.http.Normalized-User-Agent);
  }

  # If callback is not set, set to default value ""
  if (req.url.qs !~ "(?i)[^&=]*callback=([^&]+)") {
    set req.url = querystring.filter(req.url, "callback");
    set req.url = if(req.url ~ "\?", req.url "&callback=", req.url "?callback=");
  }

  # If compression is not set, use the best compression that the user-agent supports.
  if (req.url.qs !~ "(?i)[^&=]*compression=([^&]+)") {
    # When Fastly adds Brotli into the Accept-Encoding normalisation we can replace this with:
    # `set req.url = querystring.set(req.url, "compression", req.http.Accept-Encoding || "")`

    # Before SP2, IE/6 doesn't always read and cache gzipped content correctly.
    if (req.http.Fastly-Orig-Accept-Encoding && req.http.User-Agent !~ "MSIE 6") {
      if (req.http.Fastly-Orig-Accept-Encoding ~ "br") {
        set req.url = querystring.set(req.url, "compression", "br");
      } elsif (req.http.Fastly-Orig-Accept-Encoding ~ "gzip") {
        set req.url = querystring.set(req.url, "compression", "gzip");
      } else {
        set req.url = querystring.set(req.url, "compression", "");
      }
    } else {
      set req.url = querystring.set(req.url, "compression", "");
    }
  }
}

sub sort_comma_separated_value {
  # This function takes a CSV and tranforms it into a url where each
  # comma-separated-value is a query-string parameter and then uses
  # Fastly's querystring.sort function to sort the values. Once sorted
  # it then turn the query-parameters back into a CSV.
  # Set the CSV on the header `Sort-Value`.
  # Returns the sorted CSV on the header `Sorted-Value`.
  declare local var.value STRING;
  set var.value = req.http.Sort-value;

  # If query value does not exist or is empty, set it to ""
  set var.value = if(var.value != "", var.value, "");

  # Replace all `&` characters with `^`, this is because `&` would break the value up into pieces.
  set var.value = regsuball(var.value, "&", "^");

  # Replace all `,` characters with `&` to break them into individual query values
  # Append `1-` infront of all the query values to make them simpler to transform later
  set var.value = "1-" regsuball(var.value, ",", "&1-");

  # Create a querystring-like string in order for querystring.sort to work.
  set var.value = querystring.sort("?" var.value);

  # Grab all the query values from the sorted url
  set var.value = regsub(var.value, "\?", "");

  # Reverse all the previous transformations to get back the single `features` query value value
  set var.value = regsuball(var.value, "1-", "");
  set var.value = regsuball(var.value, "&", ",");
  set var.value = regsuball(var.value, "\^", "&");

  set req.http.Sorted-Value = var.value;
}

sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  call normalise_querystring_parameters_for_polyfill_bundle;
  set req.url = querystring.sort(req.url);

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

With all these functions in place the end result is that all 6 of those URLs become identical, which means they will have the same hash key inside Varnish Cache and therefore all point to the same single cached response, increasing the cache-hit ratio and decreasing the amount of requests that need to go all the way back to servers for polyfill.io.

Normalising the User-Agent header inside Varnish Cache

It made sense to explain the normalisation of the query paramaters first, but this is actually where we made the biggest step up in the cache-hit ratio. In the previous section I omitted the fact that one of the options in the API is to set the User-Agent in the URL via the ua query parameter. This is a very important feature with regard to caching because it means that we can have a different cached entry for each User-Agent making a request. However it also means that there will be a lot of cache entries and it will make the cache-hit ratio really low. The reason that would happen is because User-Agent values vary a lot. whatismybrowser.com has collected 840,000 unique User-Agents and keeps finding new ones every day.

Luckily for polyfill.io we only care about the User-Agent family, major, and minor version. In polyfill.io v1 and v2 we had an API endpoint that would take a User-Agent and return a version of it which only had the family, major, and minor version. This worked very well but introduced some complications in the VCL. Since the API endpoint was implemented in the polyfill.io server it meant that a request without a ua query parameter would first need to go to this ua-specific endpoint to find out what its normalised User-Agent value was, and then go to its original destination to return a polyfill bundle.

In v3 we have implemented this API endpoint as a function in VCL, which has removed the complications around making two requests to the polyfill.io server. The way that we parse User-Agents now is by compiling the uaparser.org into VCL for the polyfill.io service and into JS for the polyfill-library npm package.

sub useragent_parser {
  declare local var.Family STRING;
  set var.Family = "Other";
  declare local var.Major STRING;
  set var.Major = "";
  declare local var.Minor STRING;
  set var.Minor = "";
  declare local var.Patch STRING;
  set var.Patch = "";
  if (!req.http.User-Agent) {
  } else if (req.http.User-Agent ~ "(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Antenna)/(\d+) CFNetwork") {
    set var.Family = "AntennaPod";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(TopPodcasts)Pro/(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MusicDownloader)Lite/(\d+)\.(\d+)\.(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)-iPad/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)-iPhone/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(espn\.go)") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(espnradio\.com)") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "ESPN APP$") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(audioboom\.com)") {
    set var.Family = "AudioBoom";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (Rivo) RHYTHM") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CFNetwork)(?:/(\d+)\.(\d+)\.?(\d+)?)?") {
    set var.Family = "CFNetwork";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pingdom.com_bot_version_)(\d+)\.(\d+)") {
    set var.Family = "PingdomBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PingdomTMS)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "PingdomBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NewRelicPinger)/(\d+)\.(\d+)") {
    set var.Family = "NewRelicPingerBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Tableau)/(\d+)\.(\d+)") {
    set var.Family = "Tableau";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(\(StatusCake\))") {
    set var.Family = "StatusCakeBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(facebookexternalhit)/(\d+)\.(\d+)") {
    set var.Family = "FacebookBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Google.*/\+/web/snippet") {
    set var.Family = "GooglePlusBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "via ggpht.com GoogleImageProxy") {
    set var.Family = "GmailImageProxy";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Twitterbot)/(\d+)\.(\d+)") {
    set var.Family = "TwitterBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "/((?:Ant-)?Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \-](\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MSIE) (\d+)\.(\d+)([a-z]\d?)?;.* MSIECrawler") {
    set var.Family = "MSIECrawler";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(DAVdroid)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Google-HTTP-Java-Client|Apache-HttpClient|Go-http-client|scalaj-http|http%20client|Python-urllib|HttpMonitor|TLSProber|WinHTTP|JNLP|okhttp|aihttp|reqwest)(?:[ /](\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pinterest(?:bot)?)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?[;\s\(]+\+https://www.pinterest.com/bot.html") {
    set var.Family = "Pinterestbot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]+-Agent|AdsBot-Google(?:-[a-z]+)?|altavista|AppEngine-Google|archive.*?\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]+)*|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader(?: [A-Za-z]+)*|boitho.com-dc|BotSeer|BUbiNG|\b\w*favicon\w*\b|\bYeti(?:-[a-z]+)?|Catchpoint(?: bot)?|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher)?|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]+-)?Googlebot(?:-[a-zA-Z]+)?|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile)?|IconSurf|IlTrovatore(?:-Setaccio)?|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]+Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .*? Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media *)?|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]*|^NING|Nutch[^/]*|Nymesis|ObjectsSearch|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|Tiny Tiny RSS|TwitterBot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]+|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s)? Link Sleuth|Xerka [A-z]+Bot|yacy(?:bot)?|Yahoo[a-z]*Seeker|Yahoo! Slurp|Yandex\w+|YodaoBot(?:-[A-z]+)?|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Boto3?|JetS3t|aws-(?:cli|sdk-(?:cpp|go|java|nodejs|ruby2?))|s3fs)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 \-_\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*))/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 _\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*)) (\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "((?:[A-z0-9]+|[A-z\-]+ ?)?(?: the )?(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[A-Za-z0-9-]*(?:[^C][^Uu])[Bb]ot|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]*)(?:(?:[ /]| v)(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HbbTV)/(\d+)\.(\d+)\.(\d+) \(") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chimera|SeaMonkey|Camino|Waterfox)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\[FB.*;(FBAV)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = "Facebook";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\[(Pinterest)/[^\]]+\]") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pinterest)(?: for Android(?: Tablet)?)?/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Instagram).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Flipboard).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Flipboard-Briefing).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Onefootball)\/Android.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+) Basilisk/(\d+)") {
    set var.Family = "Basilisk";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PaleMoon)/(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = "Pale Moon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)(pre)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Mobile|Tablet);.*(Firefox)/(\d+)\.(\d+)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre)?)") {
    set var.Family = "Firefox (" re.group.1 ")";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(a\d+[a-z]*)") {
    set var.Family = "Firefox Alpha";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(b\d+[a-z]*)") {
    set var.Family = "Firefox Beta";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(a\d+[a-z]*)") {
    set var.Family = "Firefox Alpha";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(b\d+[a-z]*)") {
    set var.Family = "Firefox Beta";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)([ab]\d+[a-z]*)?") {
    set var.Family = "Firefox (" re.group.1 ")";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox).*Tablet browser (\d+)\.(\d+)\.(\d+)") {
    set var.Family = "MicroB";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MozillaDeveloperPreview)/(\d+)\.(\d+)([ab]\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(FxiOS)/(\d+)\.(\d+)(\.(\d+))?(\.(\d+))?") {
    set var.Family = "Firefox iOS";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Flock)/(\d+)\.(\d+)(b\d+?)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RockMelt)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Navigator)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Navigator)/(\d+)\.(\d+)([ab]\d+)") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Netscape6)/(\d+)\.(\d+)\.?([ab]?\d+)?") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MyIBrow)/(\d+)\.(\d+)") {
    set var.Family = "My Internet Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(UC? ?Browser|UCWEB|U3)[ /]?(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "UC Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera Tablet).*Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera Mini)(?:/att)?/?(\d+)?(?:\.(\d+))?(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/.+Opera Mobi.+Version/(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/(\d+)\.(\d+).+Opera Mobi") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Opera Mobi.+(Opera)(?:/|\s+)(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Opera Mobi") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/9.80.*Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Mobile Safari).*(OPR)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Chrome).*(OPR)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Opera";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Coast)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Coast";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OPiOS)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Chrome/.+( MMS)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Neon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(hpw|web)OS/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "webOS Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(luakit)") {
    set var.Family = "LuaKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Snowshoe)/(\d+)\.(\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Gecko/\d+ (Lightning)/(\d+)\.(\d+)\.?((?:[ab]?\d+[a-z]*)|(?:\d*))") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)\.(\d+(?:pre)?) \(Swiftfox\)") {
    set var.Family = "Swiftfox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)([ab]\d+[a-z]*)? \(Swiftfox\)") {
    set var.Family = "Swiftfox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(rekonq)/(\d+)\.(\d+)\.?(\d+)? Safari") {
    set var.Family = "Rekonq";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "rekonq") {
    set var.Family = "Rekonq";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(conkeror|Conkeror)/(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = "Conkeror";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(konqueror)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Konqueror";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(WeTab)-Browser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Comodo_Dragon)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Comodo Dragon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Symphony) (\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "PLAYSTATION 3.+WebKit") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "PLAYSTATION 3") {
    set var.Family = "NetFront";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayStation Portable)") {
    set var.Family = "NetFront";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayStation Vita)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "AppleWebKit.+ (NX)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nintendo 3DS)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Silk)/(\d+)\.(\d+)(?:\.([0-9\-]+))?") {
    set var.Family = "Amazon Silk";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Puffin)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Windows Phone .*(Edge)/(\d+)\.(\d+)") {
    set var.Family = "Edge Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SamsungBrowser)/(\d+)\.(\d+)") {
    set var.Family = "Samsung Internet";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SznProhlizec)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Seznam prohl%u00ED%u017Ee%u010D";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(coc_coc_browser)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Coc Coc";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(baidubrowser)[/\s](\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = "Baidu Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(FlyFlow)/(\d+)\.(\d+)") {
    set var.Family = "Baidu Explorer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MxBrowser)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Maxthon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Crosswalk)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "; wv\).+(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile WebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CrMo)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CriOS)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile iOS";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+) Mobile(?:[ /]|$)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " Mobile .*(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(chromeframe)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Frame";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SLP Browser)/(\d+)\.(\d+)") {
    set var.Family = "Tizen Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SE 2\.X) MetaSr (\d+)\.(\d+)") {
    set var.Family = "Sogou Explorer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MQQBrowser/Mini)(?:(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MQQBrowser)(?:/(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QQBrowser)(?:/(\d+)(?:\.(\d+)\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Rackspace Monitoring)/(\d+)\.(\d+)") {
    set var.Family = "RackspaceBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PyAMF)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(YaBrowser)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Yandex Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+).* MRCHROME") {
    set var.Family = "Mail.ru Chromium Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AOL) (\d+)\.(\d+); AOLBuild (\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PodCruncher|Downcast)[ /]?(\d+)\.?(\d+)?\.?(\d+)?\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (BoxNotes)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Slack_SSB)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Slack Desktop Client";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HipChat)/?(\d+)?") {
    set var.Family = "HipChat Desktop Client";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(MobileIron|FireWeb|Jasmine|ANTGalio|Midori|Fresco|Lobo|PaleMoon|Maxthon|Lynx|OmniWeb|Dillo|Camino|Demeter|Fluid|Fennec|Epiphany|Shiira|Sunrise|Spotify|Flock|Netscape|Lunascape|WebPilot|NetFront|Netfront|Konqueror|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|Opera Mini|iCab|NetNewsWire|ThunderBrowse|Iris|UP\.Browser|Bunjalloo|Google Earth|Raven for Mac|Openwave|MacOutlook|Electron|OktaMobile)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Office Outlook 12\.\d+\.\d+|MSOffice 12") {
    set var.Family = "Outlook";
    set var.Major = "2007";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook 14\.\d+\.\d+|MSOffice 14") {
    set var.Family = "Outlook";
    set var.Major = "2010";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook 15\.\d+\.\d+") {
    set var.Family = "Outlook";
    set var.Major = "2013";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook (?:Mail )?16\.\d+\.\d+") {
    set var.Family = "Outlook";
    set var.Major = "2016";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Outlook-Express\/7\.0.*") {
    set var.Family = "Windows Live Mail";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Airmail) (\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Thunderbird)/(\d+)\.(\d+)(?:\.(\d+(?:pre)?))?") {
    set var.Family = "Thunderbird";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Postbox)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Postbox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Barca(?:Pro)?)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Barca";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Lotus-Notes)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Lotus Notes";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Vivaldi)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Edge)/(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(brave)/(\d+)\.(\d+)\.(\d+) Chrome") {
    set var.Family = "Brave";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+)[\d.]* Iron[^/]") {
    set var.Family = "Iron";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Dolphin)(?: |HDCN/|/INT\-)(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HeadlessChrome)(?:/(\d+)\.(\d+)\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Evolution)/(\d+)\.(\d+)\.(\d+\.\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RCM CardDAV plugin)/(\d+)\.(\d+)\.(\d+(?:-dev)?)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(bingbot|Bolt|AdobeAIR|Jasmine|IceCat|Skyfire|Midori|Maxthon|Lynx|Arora|IBrowse|Dillo|Camino|Shiira|Fennec|Phoenix|Flock|Netscape|Lunascape|Epiphany|WebPilot|Opera Mini|Opera|NetFront|Netfront|Konqueror|Googlebot|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|iCab|iTunes|MacAppStore|NetNewsWire|Space Bison|Stainless|Orca|Dolfin|BOLT|Minimo|Tizen Browser|Polaris|Abrowser|Planetweb|ICE Browser|mDolphin|qutebrowser|Otter|QupZilla|MailBar|kmail2|YahooMobileMail|ExchangeWebServices|ExchangeServicesClient|Dragon|Outlook-iOS-Android)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chromium|Chrome)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(IEMobile)[ /](\d+)\.(\d+)") {
    set var.Family = "IE Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BacaBerita App)\/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(bPod|Pocket Casts|Player FM)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(AlexaMediaPlayer|VLC)/(\d+)\.(\d+)\.([^.\s]+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(AntennaPod|WMPlayer|Zune|Podkicker|Radio|ExoPlayerDemo|Overcast|PocketTunes|NSPlayer|okhttp|DoggCatcher|QuickNews|QuickTime|Peapod|Podcasts|GoldenPod|VLC|Spotify|Miro|MediaGo|Juice|iPodder|gPodder|Banshee)/(\d+)\.(\d+)\.?(\d+)?\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Peapod|Liferea)/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(bPod|Player FM) BMID/(\S+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Podcast ?Addict)/v(\d+) ") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Podcast ?Addict) ") {
    set var.Family = "PodcastAddict";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Replay) AV") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(VOX) Music Player") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CITA) RSS Aggregator/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pocket Casts)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Player FM)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(LG Player|Doppler|FancyMusic|MediaMonkey|Clementine) (\d+)\.(\d+)\.?([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(philpodder)/(\d+)\.(\d+)\.?([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Player FM|Pocket Casts|DoggCatcher|Spotify|MediaMonkey|MediaGo|BashPodder)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QuickTime)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kinoma)(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fancy) Cloud Music (\d+)\.(\d+)") {
    set var.Family = "FancyMusic";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "EspnDownloadManager") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ESPN) Radio (\d+)\.(\d+)\.?(\d+)? ?(?:rv:(\d+))? ") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(podracer|jPodder) v ?(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ZDM)/(\d+)\.(\d+)[; ]?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Zune|BeyondPod) (\d+)\.?(\d+)?[\);]") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(WMPlayer)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Lavf)") {
    set var.Family = "WMPlayer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(RSSRadio)[ /]?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RSS_Radio) (\d+)\.(\d+)") {
    set var.Family = "RSSRadio";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Podkicker) \S+/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Podkicker";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(HTC) Streaming Player \S+ / \S+ / \S+ / (\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Stitcher)/iOS") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Stitcher)/Android") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(VLC) .*version (\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (VLC) for") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(vlc)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "VLC";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(foobar)\S+/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Clementine)\S+ ([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(amarok)/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = "Amarok";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Custom)-Feed Reader") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iRider|Crazy Browser|SkipStone|iCab|Lunascape|Sleipnir|Maemo Browser) (\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iCab|Lunascape|Opera|Android|Jasmine|Polaris|Microsoft SkyDriveSync|The Bat!) (\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kindle)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Donut") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor="2";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Eclair") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="1";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Froyo") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="2";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Gingerbread") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="3";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Honeycomb") {
    set var.Family = re.group.1;
    set var.Major = "3";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MSIE) (\d+)\.(\d+).*XBLWP7") {
    set var.Family = "IE Large Screen";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nextcloud)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(mirall)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ownCloud-android)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Owncloud";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo)InternetBrowser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo)\-Browser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo|OBIGO)[^\d]*(\d+)(?:.(\d+))?") {
    set var.Family = "Obigo";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MAXTHON|Maxthon) (\d+)\.(\d+)") {
    set var.Family = "Maxthon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Maxthon|MyIE2|Uzbl|Shiira)") {
    set var.Family = re.group.1;
    set var.Major = "0";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BrowseX) \((\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NCSA_Mosaic)/(\d+)\.(\d+)") {
    set var.Family = "NCSA Mosaic";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(POLARIS)/(\d+)\.(\d+)") {
    set var.Family = "Polaris";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Embider)/(\d+)\.(\d+)") {
    set var.Family = "Polaris";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BonEcho)/(\d+)\.(\d+)\.?([ab]?\d+)?") {
    set var.Family = "Bon Echo";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?.*[ +]Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.* AppleNews\/\d+\.\d+\.\d+?") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.*Mobile.*[ +]Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.*Mobile") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).* Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad)") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Outlook-iOS)/\d+\.\d+\.prod\.iphone \((\d+)\.(\d+)\.(\d+)\)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AvantGo) (\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OneBrowser)/(\d+).(\d+)") {
    set var.Family = "ONE Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Avant)") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QtCarBrowser)") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(iBrowser/Mini)(\d+).(\d+)") {
    set var.Family = "iBrowser Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(iBrowser|iRAPP)/(\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Nokia)") {
    set var.Family = "Nokia Services (WAP) Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+).(\d+)\.(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+).(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BrowserNG)/(\d+)\.(\d+).(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Series60)/5\.0") {
    set var.Family = "Nokia Browser";
    set var.Major = "7";
    set var.Minor="0";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Series60)/(\d+)\.(\d+)") {
    set var.Family = "Nokia OSS Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(S40OviBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Ovi Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nokia)[EN]?(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayBook).+RIM Tablet OS (\d+)\.(\d+)\.(\d+)") {
    set var.Family = "BlackBerry WebKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Black[bB]erry|BB10).+Version/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "BlackBerry WebKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Black[bB]erry)\s?(\d+)") {
    set var.Family = "BlackBerry";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OmniWeb)/v(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Blazer)/(\d+)\.(\d+)") {
    set var.Family = "Palm Blazer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pre)/(\d+)\.(\d+)") {
    set var.Family = "Palm Pre";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ELinks)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ELinks) \((\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Links) \((\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QtWeb) Internet Browser/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PhantomJS)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AppleWebKit)/(\d+)\.?(\d+)?\+ .* Safari") {
    set var.Family = "WebKit Nightly";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Version)/(\d+)\.(\d+)(?:\.(\d+))?.*Safari/") {
    set var.Family = "Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Safari)/\d+") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OLPC)/Update(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OLPC)/Update()\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = "0";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SEMC\-Browser)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Teleca)") {
    set var.Family = "Teleca Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Phantom)/V(\d+)\.(\d+)") {
    set var.Family = "Phantom Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(7|8)\.(0)") {
    set var.Family = "IE";
    set var.Major = "11";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(6)\.(0)") {
    set var.Family = "IE";
    set var.Major = "10";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(5)\.(0)") {
    set var.Family = "IE";
    set var.Major = "9";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(4)\.(0)") {
    set var.Family = "IE";
    set var.Major = "8";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Espial)/(\d+)(?:\.(\d+))?(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AppleWebKit)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Apple Mail";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(pre|[ab]\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "([MS]?IE) (\d+)\.(\d+)") {
    set var.Family = "IE";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(python-requests)/(\d+)\.(\d+)") {
    set var.Family = "Python Requests";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Windows-Update-Agent|Microsoft-CryptoAPI|SophosUpdateManager|SophosAgent|Debian APT-HTTP|Ubuntu APT-HTTP|libcurl-agent|libwww-perl|urlgrabber|curl|PycURL|Wget|aria2|Axel|OpenBSD ftp|lftp|jupdate)(?:[ /](\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Java)[/ ]{0,1}\d+\.(\d+)\.(\d+)[_-]*([a-zA-Z0-9]+)*") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Cyberduck)/(\d+)\.(\d+)\.(\d+)(?:\.\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(S3 Browser) (\d+)-(\d+)-(\d+)(?:\s*http://s3browser\.com)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(rclone)/v(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Roku)/DVP-(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kurio)\/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Kurio App";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Box(?: Sync)?)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  }
  set req.http.useragent_parser_family=var.Family;
  set req.http.useragent_parser_major=var.Major;
  set req.http.useragent_parser_minor=var.Minor;
  set req.http.useragent_parser_patch=var.Patch;
}

sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  call useragent_parser;

  log "Family: " req.http.useragent_parser_family;
  log "Major: " req.http.useragent_parser_major;
  log "Minor: " req.http.useragent_parser_minor;
  log "Patch: " req.http.useragent_parser_patch;

  declare local var.ua STRING;
  set var.ua = req.http.useragent_parser_family "/" req.http.useragent_parser_major "." req.http.useragent_parser_minor "." req.http.useragent_parser_patch;
  set req.url = querystring.set(req.url, "ua", var.ua);

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

Normalising the User-Agent helps reduce the millions of different User-Agents down to the thousands of User-Agents that uaparser.org detects them as. We can still improve on this - currently polyfill.io supports 15 User-Agent families: Android, BlackBerry, Chrome, Edge, Edge Mobile, Firefox, Firefox Mobile, Internet Explorer, Internet Explorer Mobile, iOS Safari, iOS Chrome, Opera, Opera Mini, Opera Mobile and Samsung. If we detect that the User-Agent family is one we do not support, we can give it a generic unsupported name, such as other to ensure that all unsupported User-Agents generate the same internal URL and point to the same cached object.

sub normalise_user_agent_latest {
  if (req.http.User-Agent) {
    # Longest genuine UA seen so far: 255 chars (Facebook in-app on iOS):
    set req.http.User-Agent = if(req.http.User-Agent ~ "(^[\s\S]{0,300})", re.group.1, "other/0.0.0");

    # Remove UA tokens that unnecessarily complicate UA parsing

    # Chrome and Opera on iOS uses a UIWebView of the underlying platform to render
    # content. By stripping the CriOS or OPiOS strings, the useragent parser will alias the
    # user agent to ios_saf for the UIWebView, which is closer to the actual renderer
    set req.http.User-Agent = regsub(req.http.User-Agent, "((CriOS|OPiOS)/(\d+)\.(\d+)\.(\d+)\.(\d+)|(FxiOS/(\d+)\.(\d+)))", "");

    # # Vivaldi browser is recognised by UA module but is actually identical to Chrome, so
    # # the best way to get accurate targeting is to remove the vivaldi token from the UA
    set req.http.User-Agent = regsub(req.http.User-Agent, "(?i) vivaldi/[\d\.]+\d+", "");

    # # Facebook in-app browser `[FBAN/.....]` or `[FB_IAB/.....]` (see #990)
    set req.http.User-Agent = regsub(req.http.User-Agent, "(?i) \[(FB_IAB|FBAN|FBIOS|FB4A)/[^\]]+\]", "");

    # # Electron ` Electron/X.Y.Z` (see #1129)
    set req.http.User-Agent = regsub(req.http.User-Agent, "(?i) Electron/[\d\.]+\d+", "");

    call useragent_parser;

    # Clone the original values for later modification. This helps when debugging as it let's us see what the useragent_parser function returned.
    set req.http.normalized_user_agent_family = req.http.useragent_parser_family;
    set req.http.normalized_user_agent_major_version = req.http.useragent_parser_major;
    set req.http.normalized_user_agent_minor_version = req.http.useragent_parser_minor;
    set req.http.normalized_user_agent_patch_version = req.http.useragent_parser_patch;

    set req.http.normalized_user_agent_patch_version = "0";
    set req.http.normalized_user_agent_family = std.tolower(req.http.useragent_parser_family);

    # Aliases
    if (req.http.normalized_user_agent_family == "blackberry webkit") {
      set req.http.normalized_user_agent_family = "bb";
    }
    if (req.http.normalized_user_agent_family == "blackberry") {
      set req.http.normalized_user_agent_family = "bb";
    }
    if (req.http.normalized_user_agent_family == "pale moon (firefox variant)") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "pale moon") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "firefox mobile") {
      set req.http.normalized_user_agent_family = "firefox_mob";
    }
    if (req.http.normalized_user_agent_family == "firefox namoroka") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "firefox shiretoko") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "firefox minefield") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "firefox alpha") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "firefox beta") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "microb") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "mozilladeveloperpreview") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "iceweasel") {
      set req.http.normalized_user_agent_family = "firefox";
    }
    if (req.http.normalized_user_agent_family == "opera tablet") {
      set req.http.normalized_user_agent_family = "opera";
    }
    if (req.http.normalized_user_agent_family == "opera mobile") {
      set req.http.normalized_user_agent_family = "op_mob";
    }
    if (req.http.normalized_user_agent_family == "opera mini") {
      set req.http.normalized_user_agent_family = "op_mini";
    }
    if (req.http.normalized_user_agent_family == "chrome mobile") {
      set req.http.normalized_user_agent_family = "chrome";
    }
    if (req.http.normalized_user_agent_family == "chrome frame") {
      set req.http.normalized_user_agent_family = "chrome";
    }
    if (req.http.normalized_user_agent_family == "chromium") {
      set req.http.normalized_user_agent_family = "chrome";
    }
    if (req.http.normalized_user_agent_family == "ie mobile") {
      set req.http.normalized_user_agent_family = "ie_mob";
    }
    if (req.http.normalized_user_agent_family == "ie large screen") {
      set req.http.normalized_user_agent_family = "ie";
    }
    if (req.http.normalized_user_agent_family == "internet explorer") {
      set req.http.normalized_user_agent_family = "ie";
    }
    if (req.http.normalized_user_agent_family == "edge mobile") {
      set req.http.normalized_user_agent_family = "edge_mob";
    }
    if (req.http.normalized_user_agent_family == "uc browser") {
      if (req.http.normalized_user_agent_major_version == "9" && req.http.normalized_user_agent_minor_version == "9") {
        set req.http.normalized_user_agent_family = "ie";
        set req.http.normalized_user_agent_major_version = "10";
      }
    }
    if (req.http.normalized_user_agent_family == "chrome mobile ios") {
        set req.http.normalized_user_agent_family = "ios_chr";
      }

    if (req.http.normalized_user_agent_family == "mobile safari") {
        set req.http.normalized_user_agent_family = "ios_saf";
      }
    if (req.http.normalized_user_agent_family == "iphone") {
        set req.http.normalized_user_agent_family = "ios_saf";
      }
    if (req.http.normalized_user_agent_family == "iphone simulator") {
        set req.http.normalized_user_agent_family = "ios_saf";
      }
    if (req.http.normalized_user_agent_family == "mobile safari uiwebview") {
        set req.http.normalized_user_agent_family = "ios_saf";
      }
    if (req.http.normalized_user_agent_family == "mobile safari ui/wkwebview") {
        set req.http.normalized_user_agent_family = "ios_saf";
      }

    if (req.http.normalized_user_agent_family == "samsung internet") {
        set req.http.normalized_user_agent_family = "samsung_mob";
      }

    if (req.http.normalized_user_agent_family == "phantomjs") {
        set req.http.normalized_user_agent_family = "safari";
        set req.http.normalized_user_agent_major_version = "5";
      }

    if (req.http.normalized_user_agent_family == "yandex browser") {
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "37";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "36";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "35";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "34";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "33";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "32";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "30";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "28";
      }
      if (req.http.normalized_user_agent_major_version == "14" && req.http.normalized_user_agent_minor_version == "10") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "60";
      }
    }

    if (req.http.normalized_user_agent_family == "opera") {
      if (req.http.normalized_user_agent_major_version == "20") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "33";
      }
      if (req.http.normalized_user_agent_major_version == "21") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "34";
      }
      if (req.http.normalized_user_agent_major_version == "22") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "35";
      }
      if (req.http.normalized_user_agent_major_version == "23") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "36";
      }
      if (req.http.normalized_user_agent_major_version == "24") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "37";
      }
      if (req.http.normalized_user_agent_major_version == "25") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "38";
      }
      if (req.http.normalized_user_agent_major_version == "26") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "39";
      }
      if (req.http.normalized_user_agent_major_version == "27") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "40";
      }
      if (req.http.normalized_user_agent_major_version == "28") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "41";
      }
      if (req.http.normalized_user_agent_major_version == "29") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "42";
      }
      if (req.http.normalized_user_agent_major_version == "30") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "43";
      }
      if (req.http.normalized_user_agent_major_version == "31") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "44";
      }
      if (req.http.normalized_user_agent_major_version == "32") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "45";
      }
      if (req.http.normalized_user_agent_major_version == "33") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "46";
      }
      if (req.http.normalized_user_agent_major_version == "34") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "47";
      }
      if (req.http.normalized_user_agent_major_version == "35") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "48";
      }
      if (req.http.normalized_user_agent_major_version == "36") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "49";
      }
      if (req.http.normalized_user_agent_major_version == "37") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "50";
      }
      if (req.http.normalized_user_agent_major_version == "38") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "51";
      }
      if (req.http.normalized_user_agent_major_version == "39") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "52";
      }
      if (req.http.normalized_user_agent_major_version == "40") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "53";
      }
      if (req.http.normalized_user_agent_major_version == "41") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "54";
      }
      if (req.http.normalized_user_agent_major_version == "42") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "55";
      }
      if (req.http.normalized_user_agent_major_version == "43") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "56";
      }
      if (req.http.normalized_user_agent_major_version == "44") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "57";
      }
      if (req.http.normalized_user_agent_major_version == "45") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "58";
      }
      if (req.http.normalized_user_agent_major_version == "46") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "59";
      }
      if (req.http.normalized_user_agent_major_version == "47") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "60";
      }
    }

    if (req.http.normalized_user_agent_family == "googlebot") {
      if (req.http.normalized_user_agent_major_version == "2" && req.http.normalized_user_agent_minor_version == "1") {
        set req.http.normalized_user_agent_family = "chrome";
        set req.http.normalized_user_agent_major_version = "41";
      }
    }

    # Supported Browsers and minimum supported versions.
    if (
      # "edge": "*",
      (req.http.normalized_user_agent_family == "edge") ||
      # "edge_mob": "*",
      (req.http.normalized_user_agent_family == "edge_mob") ||
      # "ie": ">=8",
      (req.http.normalized_user_agent_family == "ie" && std.atoi(req.http.normalized_user_agent_major_version) >= 8) ||
      # "ie_mob": ">=11",
      (req.http.normalized_user_agent_family == "ie_mob" && std.atoi(req.http.normalized_user_agent_major_version) >= 11) ||
      # "chrome": ">=29",
      (req.http.normalized_user_agent_family == "chrome" && std.atoi(req.http.normalized_user_agent_major_version) >= 29) ||
      # "safari": ">=9",
      (req.http.normalized_user_agent_family == "safari" && std.atoi(req.http.normalized_user_agent_major_version) >= 9) ||
      # "ios_saf": ">=9",
      (req.http.normalized_user_agent_family == "ios_saf" && std.atoi(req.http.normalized_user_agent_major_version) >= 9) ||
      # "ios_chr": ">=9",
      (req.http.normalized_user_agent_family == "ios_chr" && std.atoi(req.http.normalized_user_agent_major_version) >= 9) ||
      # "firefox": ">=38",
      (req.http.normalized_user_agent_family == "firefox" && std.atoi(req.http.normalized_user_agent_major_version) >= 38) ||
      # "firefox_mob": ">=38",
      (req.http.normalized_user_agent_family == "firefox_mob" && std.atoi(req.http.normalized_user_agent_major_version) >= 38) ||
      # "android": ">=4.3",
      (req.http.normalized_user_agent_family == "android" && std.atoi(req.http.normalized_user_agent_major_version) >= 5) ||
      (req.http.normalized_user_agent_family == "android" && std.atoi(req.http.normalized_user_agent_major_version) == 4 && std.atoi(req.http.normalized_user_agent_minor_version) >= 3) ||
      # "opera": ">=33",
      (req.http.normalized_user_agent_family == "opera" && std.atoi(req.http.normalized_user_agent_major_version) >= 33) ||
      # "op_mob": ">=10",
      (req.http.normalized_user_agent_family == "op_mob" && std.atoi(req.http.normalized_user_agent_major_version) >= 10) ||
      # "op_mini": ">=5",
      (req.http.normalized_user_agent_family == "op_mini" && std.atoi(req.http.normalized_user_agent_major_version) >= 5) ||
      # "bb": ">=6",
      (req.http.normalized_user_agent_family == "bb" && std.atoi(req.http.normalized_user_agent_major_version) >= 6) ||
      # "samsung_mob": ">=4"
      (req.http.normalized_user_agent_family == "samsung_mob" && std.atoi(req.http.normalized_user_agent_major_version) >= 4)
    ) {
      set req.http.Normalized-User-Agent = req.http.normalized_user_agent_family "/"  req.http.normalized_user_agent_major_version "." req.http.normalized_user_agent_minor_version "." req.http.normalized_user_agent_patch_version;
    } else {
      set req.http.normalized_user_agent_family = "other";
      set req.http.normalized_user_agent_major_version = "0";
      set req.http.normalized_user_agent_minor_version = "0";
      set req.http.Normalized-User-Agent = req.http.normalized_user_agent_family "/"  req.http.normalized_user_agent_major_version "." req.http.normalized_user_agent_minor_version "." req.http.normalized_user_agent_patch_version;
    }
  } else {
    set req.http.Normalized-User-Agent = "other/0.0.0";
  }
}

sub useragent_parser {
  declare local var.Family STRING;
  set var.Family = "Other";
  declare local var.Major STRING;
  set var.Major = "";
  declare local var.Minor STRING;
  set var.Minor = "";
  declare local var.Patch STRING;
  set var.Patch = "";
  if (!req.http.User-Agent) {
  } else if (req.http.User-Agent ~ "(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Antenna)/(\d+) CFNetwork") {
    set var.Family = "AntennaPod";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(TopPodcasts)Pro/(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MusicDownloader)Lite/(\d+)\.(\d+)\.(\d+) CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)-iPad/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)-iPhone/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(.*)/(\d+)\.?(\d+)?.?(\d+)?.?(\d+)? CFNetwork") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(espn\.go)") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(espnradio\.com)") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "ESPN APP$") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(audioboom\.com)") {
    set var.Family = "AudioBoom";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (Rivo) RHYTHM") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CFNetwork)(?:/(\d+)\.(\d+)\.?(\d+)?)?") {
    set var.Family = "CFNetwork";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pingdom.com_bot_version_)(\d+)\.(\d+)") {
    set var.Family = "PingdomBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PingdomTMS)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "PingdomBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NewRelicPinger)/(\d+)\.(\d+)") {
    set var.Family = "NewRelicPingerBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Tableau)/(\d+)\.(\d+)") {
    set var.Family = "Tableau";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(\(StatusCake\))") {
    set var.Family = "StatusCakeBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(facebookexternalhit)/(\d+)\.(\d+)") {
    set var.Family = "FacebookBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Google.*/\+/web/snippet") {
    set var.Family = "GooglePlusBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "via ggpht.com GoogleImageProxy") {
    set var.Family = "GmailImageProxy";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Twitterbot)/(\d+)\.(\d+)") {
    set var.Family = "TwitterBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "/((?:Ant-)?Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \-](\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MSIE) (\d+)\.(\d+)([a-z]\d?)?;.* MSIECrawler") {
    set var.Family = "MSIECrawler";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(DAVdroid)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Google-HTTP-Java-Client|Apache-HttpClient|Go-http-client|scalaj-http|http%20client|Python-urllib|HttpMonitor|TLSProber|WinHTTP|JNLP|okhttp|aihttp|reqwest)(?:[ /](\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pinterest(?:bot)?)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?[;\s\(]+\+https://www.pinterest.com/bot.html") {
    set var.Family = "Pinterestbot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]+-Agent|AdsBot-Google(?:-[a-z]+)?|altavista|AppEngine-Google|archive.*?\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]+)*|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader(?: [A-Za-z]+)*|boitho.com-dc|BotSeer|BUbiNG|\b\w*favicon\w*\b|\bYeti(?:-[a-z]+)?|Catchpoint(?: bot)?|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher)?|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]+-)?Googlebot(?:-[a-zA-Z]+)?|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile)?|IconSurf|IlTrovatore(?:-Setaccio)?|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]+Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .*? Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media *)?|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]*|^NING|Nutch[^/]*|Nymesis|ObjectsSearch|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|Tiny Tiny RSS|TwitterBot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]+|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s)? Link Sleuth|Xerka [A-z]+Bot|yacy(?:bot)?|Yahoo[a-z]*Seeker|Yahoo! Slurp|Yandex\w+|YodaoBot(?:-[A-z]+)?|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Boto3?|JetS3t|aws-(?:cli|sdk-(?:cpp|go|java|nodejs|ruby2?))|s3fs)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 \-_\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*))/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 _\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*)) (\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "((?:[A-z0-9]+|[A-z\-]+ ?)?(?: the )?(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[A-Za-z0-9-]*(?:[^C][^Uu])[Bb]ot|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]*)(?:(?:[ /]| v)(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HbbTV)/(\d+)\.(\d+)\.(\d+) \(") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chimera|SeaMonkey|Camino|Waterfox)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\[FB.*;(FBAV)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = "Facebook";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\[(Pinterest)/[^\]]+\]") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pinterest)(?: for Android(?: Tablet)?)?/(\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Instagram).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Flipboard).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Flipboard-Briefing).(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Mozilla.*Mobile.*(Onefootball)\/Android.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+) Basilisk/(\d+)") {
    set var.Family = "Basilisk";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PaleMoon)/(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = "Pale Moon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)(pre)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fennec)/(\d+)\.(\d+)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Mobile|Tablet);.*(Firefox)/(\d+)\.(\d+)") {
    set var.Family = "Firefox Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre)?)") {
    set var.Family = "Firefox (" re.group.1 ")";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(a\d+[a-z]*)") {
    set var.Family = "Firefox Alpha";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(b\d+[a-z]*)") {
    set var.Family = "Firefox Beta";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(a\d+[a-z]*)") {
    set var.Family = "Firefox Alpha";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(b\d+[a-z]*)") {
    set var.Family = "Firefox Beta";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)([ab]\d+[a-z]*)?") {
    set var.Family = "Firefox (" re.group.1 ")";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox).*Tablet browser (\d+)\.(\d+)\.(\d+)") {
    set var.Family = "MicroB";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MozillaDeveloperPreview)/(\d+)\.(\d+)([ab]\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(FxiOS)/(\d+)\.(\d+)(\.(\d+))?(\.(\d+))?") {
    set var.Family = "Firefox iOS";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Flock)/(\d+)\.(\d+)(b\d+?)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RockMelt)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Navigator)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Navigator)/(\d+)\.(\d+)([ab]\d+)") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Netscape6)/(\d+)\.(\d+)\.?([ab]?\d+)?") {
    set var.Family = "Netscape";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MyIBrow)/(\d+)\.(\d+)") {
    set var.Family = "My Internet Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(UC? ?Browser|UCWEB|U3)[ /]?(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "UC Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera Tablet).*Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera Mini)(?:/att)?/?(\d+)?(?:\.(\d+))?(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/.+Opera Mobi.+Version/(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/(\d+)\.(\d+).+Opera Mobi") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Opera Mobi.+(Opera)(?:/|\s+)(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Opera Mobi") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Opera)/9.80.*Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Mobile Safari).*(OPR)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Opera Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(?:Chrome).*(OPR)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Opera";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Coast)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Coast";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OPiOS)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Chrome/.+( MMS)/(\d+).(\d+).(\d+)") {
    set var.Family = "Opera Neon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(hpw|web)OS/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "webOS Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(luakit)") {
    set var.Family = "LuaKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Snowshoe)/(\d+)\.(\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Gecko/\d+ (Lightning)/(\d+)\.(\d+)\.?((?:[ab]?\d+[a-z]*)|(?:\d*))") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)\.(\d+(?:pre)?) \(Swiftfox\)") {
    set var.Family = "Swiftfox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)([ab]\d+[a-z]*)? \(Swiftfox\)") {
    set var.Family = "Swiftfox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(rekonq)/(\d+)\.(\d+)\.?(\d+)? Safari") {
    set var.Family = "Rekonq";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "rekonq") {
    set var.Family = "Rekonq";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(conkeror|Conkeror)/(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = "Conkeror";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(konqueror)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Konqueror";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(WeTab)-Browser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Comodo_Dragon)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Comodo Dragon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Symphony) (\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "PLAYSTATION 3.+WebKit") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "PLAYSTATION 3") {
    set var.Family = "NetFront";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayStation Portable)") {
    set var.Family = "NetFront";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayStation Vita)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "AppleWebKit.+ (NX)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nintendo 3DS)") {
    set var.Family = "NetFront NX";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Silk)/(\d+)\.(\d+)(?:\.([0-9\-]+))?") {
    set var.Family = "Amazon Silk";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Puffin)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Windows Phone .*(Edge)/(\d+)\.(\d+)") {
    set var.Family = "Edge Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SamsungBrowser)/(\d+)\.(\d+)") {
    set var.Family = "Samsung Internet";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SznProhlizec)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Seznam prohl%u00ED%u017Ee%u010D";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(coc_coc_browser)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Coc Coc";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(baidubrowser)[/\s](\d+)(?:\.(\d+)(?:\.(\d+))?)?") {
    set var.Family = "Baidu Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(FlyFlow)/(\d+)\.(\d+)") {
    set var.Family = "Baidu Explorer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MxBrowser)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Maxthon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Crosswalk)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "; wv\).+(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile WebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CrMo)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CriOS)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile iOS";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+) Mobile(?:[ /]|$)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " Mobile .*(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(chromeframe)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Chrome Frame";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SLP Browser)/(\d+)\.(\d+)") {
    set var.Family = "Tizen Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SE 2\.X) MetaSr (\d+)\.(\d+)") {
    set var.Family = "Sogou Explorer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MQQBrowser/Mini)(?:(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MQQBrowser)(?:/(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QQBrowser)(?:/(\d+)(?:\.(\d+)\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = "QQ Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Rackspace Monitoring)/(\d+)\.(\d+)") {
    set var.Family = "RackspaceBot";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PyAMF)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(YaBrowser)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Yandex Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+).* MRCHROME") {
    set var.Family = "Mail.ru Chromium Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AOL) (\d+)\.(\d+); AOLBuild (\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PodCruncher|Downcast)[ /]?(\d+)\.?(\d+)?\.?(\d+)?\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (BoxNotes)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Slack_SSB)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Slack Desktop Client";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HipChat)/?(\d+)?") {
    set var.Family = "HipChat Desktop Client";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(MobileIron|FireWeb|Jasmine|ANTGalio|Midori|Fresco|Lobo|PaleMoon|Maxthon|Lynx|OmniWeb|Dillo|Camino|Demeter|Fluid|Fennec|Epiphany|Shiira|Sunrise|Spotify|Flock|Netscape|Lunascape|WebPilot|NetFront|Netfront|Konqueror|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|Opera Mini|iCab|NetNewsWire|ThunderBrowse|Iris|UP\.Browser|Bunjalloo|Google Earth|Raven for Mac|Openwave|MacOutlook|Electron|OktaMobile)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Office Outlook 12\.\d+\.\d+|MSOffice 12") {
    set var.Family = "Outlook";
    set var.Major = "2007";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook 14\.\d+\.\d+|MSOffice 14") {
    set var.Family = "Outlook";
    set var.Major = "2010";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook 15\.\d+\.\d+") {
    set var.Family = "Outlook";
    set var.Major = "2013";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Microsoft Outlook (?:Mail )?16\.\d+\.\d+") {
    set var.Family = "Outlook";
    set var.Major = "2016";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "Outlook-Express\/7\.0.*") {
    set var.Family = "Windows Live Mail";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Airmail) (\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Thunderbird)/(\d+)\.(\d+)(?:\.(\d+(?:pre)?))?") {
    set var.Family = "Thunderbird";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Postbox)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Postbox";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Barca(?:Pro)?)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Barca";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Lotus-Notes)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Lotus Notes";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Vivaldi)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Edge)/(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(brave)/(\d+)\.(\d+)\.(\d+) Chrome") {
    set var.Family = "Brave";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chrome)/(\d+)\.(\d+)\.(\d+)[\d.]* Iron[^/]") {
    set var.Family = "Iron";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Dolphin)(?: |HDCN/|/INT\-)(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(HeadlessChrome)(?:/(\d+)\.(\d+)\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Evolution)/(\d+)\.(\d+)\.(\d+\.\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RCM CardDAV plugin)/(\d+)\.(\d+)\.(\d+(?:-dev)?)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(bingbot|Bolt|AdobeAIR|Jasmine|IceCat|Skyfire|Midori|Maxthon|Lynx|Arora|IBrowse|Dillo|Camino|Shiira|Fennec|Phoenix|Flock|Netscape|Lunascape|Epiphany|WebPilot|Opera Mini|Opera|NetFront|Netfront|Konqueror|Googlebot|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|iCab|iTunes|MacAppStore|NetNewsWire|Space Bison|Stainless|Orca|Dolfin|BOLT|Minimo|Tizen Browser|Polaris|Abrowser|Planetweb|ICE Browser|mDolphin|qutebrowser|Otter|QupZilla|MailBar|kmail2|YahooMobileMail|ExchangeWebServices|ExchangeServicesClient|Dragon|Outlook-iOS-Android)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Chromium|Chrome)/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(IEMobile)[ /](\d+)\.(\d+)") {
    set var.Family = "IE Mobile";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BacaBerita App)\/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(bPod|Pocket Casts|Player FM)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(AlexaMediaPlayer|VLC)/(\d+)\.(\d+)\.([^.\s]+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(AntennaPod|WMPlayer|Zune|Podkicker|Radio|ExoPlayerDemo|Overcast|PocketTunes|NSPlayer|okhttp|DoggCatcher|QuickNews|QuickTime|Peapod|Podcasts|GoldenPod|VLC|Spotify|Miro|MediaGo|Juice|iPodder|gPodder|Banshee)/(\d+)\.(\d+)\.?(\d+)?\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Peapod|Liferea)/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(bPod|Player FM) BMID/(\S+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Podcast ?Addict)/v(\d+) ") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Podcast ?Addict) ") {
    set var.Family = "PodcastAddict";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Replay) AV") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(VOX) Music Player") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(CITA) RSS Aggregator/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pocket Casts)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Player FM)$") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(LG Player|Doppler|FancyMusic|MediaMonkey|Clementine) (\d+)\.(\d+)\.?([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(philpodder)/(\d+)\.(\d+)\.?([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Player FM|Pocket Casts|DoggCatcher|Spotify|MediaMonkey|MediaGo|BashPodder)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QuickTime)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kinoma)(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Fancy) Cloud Music (\d+)\.(\d+)") {
    set var.Family = "FancyMusic";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "EspnDownloadManager") {
    set var.Family = "ESPN";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ESPN) Radio (\d+)\.(\d+)\.?(\d+)? ?(?:rv:(\d+))? ") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(podracer|jPodder) v ?(\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ZDM)/(\d+)\.(\d+)[; ]?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Zune|BeyondPod) (\d+)\.?(\d+)?[\);]") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(WMPlayer)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Lavf)") {
    set var.Family = "WMPlayer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(RSSRadio)[ /]?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(RSS_Radio) (\d+)\.(\d+)") {
    set var.Family = "RSSRadio";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Podkicker) \S+/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Podkicker";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(HTC) Streaming Player \S+ / \S+ / \S+ / (\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Stitcher)/iOS") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Stitcher)/Android") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(VLC) .*version (\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ " (VLC) for") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(vlc)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "VLC";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(foobar)\S+/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Clementine)\S+ ([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(amarok)/([^.\s]+)\.([^.\s]+)?\.?([^.\s]+)?") {
    set var.Family = "Amarok";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Custom)-Feed Reader") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iRider|Crazy Browser|SkipStone|iCab|Lunascape|Sleipnir|Maemo Browser) (\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iCab|Lunascape|Opera|Android|Jasmine|Polaris|Microsoft SkyDriveSync|The Bat!) (\d+)\.(\d+)\.?(\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kindle)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Donut") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor="2";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Eclair") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="1";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Froyo") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="2";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Gingerbread") {
    set var.Family = re.group.1;
    set var.Major = "2";
    set var.Minor="3";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Android) Honeycomb") {
    set var.Family = re.group.1;
    set var.Major = "3";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MSIE) (\d+)\.(\d+).*XBLWP7") {
    set var.Family = "IE Large Screen";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nextcloud)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(mirall)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ownCloud-android)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Owncloud";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo)InternetBrowser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo)\-Browser") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Obigo|OBIGO)[^\d]*(\d+)(?:.(\d+))?") {
    set var.Family = "Obigo";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(MAXTHON|Maxthon) (\d+)\.(\d+)") {
    set var.Family = "Maxthon";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Maxthon|MyIE2|Uzbl|Shiira)") {
    set var.Family = re.group.1;
    set var.Major = "0";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BrowseX) \((\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NCSA_Mosaic)/(\d+)\.(\d+)") {
    set var.Family = "NCSA Mosaic";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(POLARIS)/(\d+)\.(\d+)") {
    set var.Family = "Polaris";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Embider)/(\d+)\.(\d+)") {
    set var.Family = "Polaris";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BonEcho)/(\d+)\.(\d+)\.?([ab]?\d+)?") {
    set var.Family = "Bon Echo";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?.*[ +]Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.* AppleNews\/\d+\.\d+\.\d+?") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.*Mobile.*[ +]Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPod touch|iPhone|iPad);.*CPU.*OS[ +](\d+)_(\d+)(?:_(\d+))?.*Mobile") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad).* Safari") {
    set var.Family = "Mobile Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(iPod|iPhone|iPad)") {
    set var.Family = "Mobile Safari UI/WKWebView";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Outlook-iOS)/\d+\.\d+\.prod\.iphone \((\d+)\.(\d+)\.(\d+)\)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AvantGo) (\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OneBrowser)/(\d+).(\d+)") {
    set var.Family = "ONE Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Avant)") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QtCarBrowser)") {
    set var.Family = re.group.1;
    set var.Major = "1";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(iBrowser/Mini)(\d+).(\d+)") {
    set var.Family = "iBrowser Mini";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(iBrowser|iRAPP)/(\d+).(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Nokia)") {
    set var.Family = "Nokia Services (WAP) Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+).(\d+)\.(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+).(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(NokiaBrowser)/(\d+)\.(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(BrowserNG)/(\d+)\.(\d+).(\d+)") {
    set var.Family = "Nokia Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Series60)/5\.0") {
    set var.Family = "Nokia Browser";
    set var.Major = "7";
    set var.Minor="0";
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Series60)/(\d+)\.(\d+)") {
    set var.Family = "Nokia OSS Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(S40OviBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Ovi Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Nokia)[EN]?(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PlayBook).+RIM Tablet OS (\d+)\.(\d+)\.(\d+)") {
    set var.Family = "BlackBerry WebKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Black[bB]erry|BB10).+Version/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "BlackBerry WebKit";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Black[bB]erry)\s?(\d+)") {
    set var.Family = "BlackBerry";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OmniWeb)/v(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Blazer)/(\d+)\.(\d+)") {
    set var.Family = "Palm Blazer";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Pre)/(\d+)\.(\d+)") {
    set var.Family = "Palm Pre";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ELinks)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(ELinks) \((\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Links) \((\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(QtWeb) Internet Browser/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(PhantomJS)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AppleWebKit)/(\d+)\.?(\d+)?\+ .* Safari") {
    set var.Family = "WebKit Nightly";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Version)/(\d+)\.(\d+)(?:\.(\d+))?.*Safari/") {
    set var.Family = "Safari";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Safari)/\d+") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OLPC)/Update(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(OLPC)/Update()\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = "0";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(SEMC\-Browser)/(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Teleca)") {
    set var.Family = "Teleca Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Phantom)/V(\d+)\.(\d+)") {
    set var.Family = "Phantom Browser";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(7|8)\.(0)") {
    set var.Family = "IE";
    set var.Major = "11";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(6)\.(0)") {
    set var.Family = "IE";
    set var.Major = "10";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(5)\.(0)") {
    set var.Family = "IE";
    set var.Major = "9";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Trident)/(4)\.(0)") {
    set var.Family = "IE";
    set var.Major = "8";
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Espial)/(\d+)(?:\.(\d+))?(?:\.(\d+))?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(AppleWebKit)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Apple Mail";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Firefox)/(\d+)\.(\d+)(pre|[ab]\d+[a-z]*)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "([MS]?IE) (\d+)\.(\d+)") {
    set var.Family = "IE";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(python-requests)/(\d+)\.(\d+)") {
    set var.Family = "Python Requests";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "\b(Windows-Update-Agent|Microsoft-CryptoAPI|SophosUpdateManager|SophosAgent|Debian APT-HTTP|Ubuntu APT-HTTP|libcurl-agent|libwww-perl|urlgrabber|curl|PycURL|Wget|aria2|Axel|OpenBSD ftp|lftp|jupdate)(?:[ /](\d+)(?:\.(\d+)(?:\.(\d+))?)?)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Java)[/ ]{0,1}\d+\.(\d+)\.(\d+)[_-]*([a-zA-Z0-9]+)*") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Cyberduck)/(\d+)\.(\d+)\.(\d+)(?:\.\d+)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(S3 Browser) (\d+)-(\d+)-(\d+)(?:\s*http://s3browser\.com)?") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(rclone)/v(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Roku)/DVP-(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "(Kurio)\/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = "Kurio App";
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  } else if (req.http.User-Agent ~ "^(Box(?: Sync)?)/(\d+)\.(\d+)\.(\d+)") {
    set var.Family = re.group.1;
    set var.Major = re.group.2;
    set var.Minor = re.group.3;
    set var.Patch = re.group.4;
  }
  set req.http.useragent_parser_family=var.Family;
  set req.http.useragent_parser_major=var.Major;
  set req.http.useragent_parser_minor=var.Minor;
  set req.http.useragent_parser_patch=var.Patch;
}
sub vcl_recv {
  # Store original url for logging purposes.
  declare local var.original-url STRING;
  set var.original-url = req.url;

  call normalise_user_agent_latest;

  log "Normalized-User-Agent: " req.http.Normalized-User-Agent;

  set req.url = querystring.set(req.url, "ua", req.http.Normalized-User-Agent);

  log "Original url: " var.original-url;
  log "Updated  url: " req.url;
}

As I said, this change is the one that brought about the biggest improvement in our cache-hit ratio. Instead of getting a different cache-entry for every browser family and version, we only get different cache-entries for browser family and versions we support. As of writing this article, that works out to be roughly 300 different cache entries (Chrome has roughly 70 releases, Edge has 6 etc).