SiteHub

CircleCI Code Climate Test Coverage

SiteHub is a HTTP proxy written in Ruby with support for A|B testing baked in. SiteHub sits in front of your web application(s) routing all HTTP traffics between it and your users.

Wouldn't it be cool to write something like:

sitehub = SiteHub.build do 
  proxy %r{/catalogue/(.*)} do
    split percentage: 50, url: 'http://version1.com/$1', label: :prototype_1
    split percentage: 50, url: 'http://version2.com/$1', label: :prototype_2
  end
end

run sitehub

or

user_eligible = proc{} #perform check and return boolean

sitehub = SiteHub.build do 
  proxy %r{/catalogue/(.*)} do
    route url: 'http://new_catalogue.com/catalogue/$1', label: :new_version, rule: user_eligbile 

    default url: 'http://current_catalogue.com/$1'
  end
  # other proxy definitions ...
end

run sitehub

With SiteHub you can:

  • A|B testing new features
  • Silently release new features - SiteHub can be used to put a new version of you application live but inaccessible to users. This new version can still be accessed by teams to peform final checks before opening up the new version to the public.
  • Modular Web Applications - SiteHub can be used to front discrete applications and present a unified root.

Installation

gem install sitehub

Definning a SiteHub

A SiteHub is a rack application so needs to be passed to the run method in your rackup file

example config.ru

require 'sitehub'
sitehub = SiteHub.build do
  proxy '/' => 'http://downstream.url.com'
end
run sitehub

Defining proxies

Proxies can have either routes or splits defined within them but not both at the same time.

  • Splits - define the percentage chance that a downstream url will be used to proxy a user request.
  • Routes - routes are defined with a rule that determines whether or not a user request can be sent to its downstream url

Version affinity

Once a downstream route has been chosen for a given route it is sticky. Meaning that users will stay with a version and not flip flop between them.

Sitehub does this by dropping a cookie that holds the route version that a request is sent to.

By default the cookie will be given the name sitehub.recorded_route and will have the path of the request.

Overiding the name

This is done at the top level of your sitehub definition. The name you supply will be used for all sitehub cookies dropped by all proxies.

sitehub = SiteHub.build do
  sitehub_cookie_name :your_custom_name
end

Overiding the path

This is done a proxy by proxy basis

sitehub = SiteHub.build do
  proxy '/' do
    sitehub_cookie_path '/your/path'
    #splits/routes defined here
  end
end

Caution: By default sitehub is going to use the path of the request. If you have used a regular expression to define a proxy, this will be different for each unique request that is made.

e.g. for the following example, calls to /path1, and /path2 would each be given a cookie meaning that users could flip between different version for the same proxy definition. In this case you are definately going to want to set the sitehub_cookie_path to keep things consistent.

sitehub = SiteHub.build do
  proxy %r{/*} do
    #splits/routes defined here
  end
end

Routes with Rules

Define a route inside a proxy defintion as follows

sitehub = SiteHub.build do 
  proxy '/catalogue' do
    route url: 'http://new_catalogue.com/catalogue', label: :new_version, rule: user_eligbile
    # ...
  end
end

Rules must be an object that responds to call with a single parameter that returns a boolean. True means that the rule applies and false means that it does not.

The parameter passed to call is the request environment hash. This lets you write things like:

has_special_parameter = proc do |env|
  Rack::Request.new(env).params.include?(:special_param)
end

Splits

Splits are defined as follows

sitehub = SiteHub.build do 
  proxy '/catalogue' do
    split percentage: 50, url: 'http://version1.com', label: :prototype_1
    split percentage: 50, url: 'http://version2.com', label: :prototype_2
  end
end

Split percentages must add up to 100% unless a default is defined.

Default routes

When defining either Splits or Routes a default can be defined. Defaults are used as a fallback if a route with a rule that applies can't be found or a split can't be chosen on the first time of trying (This can happen when the splits don't add up to 100%).

sitehub = SiteHub.build do 
  proxy '/catalogue' do
    route url: 'http://new_catalogue.com/catalogue', label: :new_version, rule: a_rule
    default url: 'http://current_catalogue.com'
  end
end

Nesting routes and splits

Routes and Splits can themselves contain further route or split definitions

sitehub = SiteHub.build do 
  proxy '/catalogue' do
    # Experiment 1
    split(precentage: 30) do
      # 30% of overall traffic is split between 2 different prototypes
      split percentage: 50, url: 'http://version1.com', label: :prototype_1
      split percentage: 50, url: 'http://version2.com', label: :prototype_2
    end

    # Experiment 2
    split(precentage: 30) do
      # 30% of overall traffic is split between 2 different prototypes
      split percentage: 50, url: 'http://version3.com', label: :prototype_3
      split percentage: 50, url: 'http://version4.com', label: :prototype_4
    end

    default url: 'http://current_catalogue.com'
  end
end

Labels

Splits and Routes must be defined with a label. Within a proxy defintion, this label must be unique. This is the value that SiteHub will use to identify the version of a downstream url that a user should stick to once it has been selected.

Matching

Proxy can be defined to capture specific paths using a literal string or be defined to have a broader appeal using regexs

sitehub = SiteHub.build do 
  proxy '/catalogue' => 'http://downstream.catalogue.com'
  proxy %r{/orders/*} => 'http://downstream.orders.com'
end

Substitution

Portions of the request path can be captured and passed downstream by specifying capture groups your path regular expression.

sitehub = SiteHub.build do 
  proxy %r{'/orders/(.*)} => 'http://downstream.orders.com/$1'
end

Using middleware

You can use middleware in conjunction with a particular proxy definition or the SiteHub as a whole.

Proxy specifc middleware

sitehub = SiteHub.build do 
  proxy '/catalogue/(.*)' do
    use AuthenticationMiddlware
  end
end

In this example, only requests received by the proxy definition will be made to go through the AuthenticationMiddleware

SiteHub wide middlware

sitehub = SiteHub.build do 
  use AuthenticationMiddleware
  proxy '/catalogue' => 'http://downstream.catalogue.com'
  proxy '/orders' => 'http://downstream.orders.com'
end

In this example, all requests handled by the SiteHub will go through the AuthenticationMiddleware

Reverse Proxying

In order to ensure that you applications stay firmly behind your SiteHub, you are going need to ensure that any responses that they return have are rewritten to remove references to your downstream URLs. SiteHub does this for the Location header (set for redirects) and will soon do it for the Content-Location header also.

sitehub = SiteHub.build do
  proxy %r{/orders/(.*)} => 'http://downstream.orders.com/$1'
  reverse_proxy %r{http://downstream.orders.com/(.*)} => '/orders/$1'
end

Note The above example also performs substitution from the downstream URL in to the upstream mapping. This is not mandatory

SiteHub Transaction ID

SiteHub introduces a custom header to downstream requests called sitehub_transaction_id that is unique to every request. The idea is that if a request is made from the downstream system to another then this header should be passed on also. If access/errors in each system are logged along with this id, then tracing things in distributed systems will become easier.

The transaction id, if passed on through out, could also be used for request scoped caching.

Logging

By default SiteHub will log requests and errors to STDOUT and STDERR respectively. You can overide this with your own logging devices. For example you may want to send requests and errors to syslog. Just make sure that your logger object responds to << or write and SiteHub will do the rest.

sitehub = SiteHub.build do
  access_logger YourLogger.new
  error_logger YourLogger.new
end