Home

Awesome

WebServerUid

WebServerUid is a small gem that can be used to represent "UIDs" in your application, where a "UID" is a unique ID generated by Apache's mod_uid or nginx's http_userid_module. Using these modules, you can generate a unique ID for each visitor to your website, before they log in, and add it to all logged data, from database rows to application logs to web-server logs to anything else you may desire.

Generating a unique ID for each visitor is not terribly difficult and can easily be done within (e.g.) Rails, by simply creating a large unique value (like a UUID) and assigning it to a cookie. However, there is one huge caveat to this approach: the very first request the visitor makes to your site will not be tagged in your web-server logs with this unique ID, because they will not have had the cookie present in their browser when they made the request. This initial request is critical, because it contains the HTTP referer (i.e., how the user got to your site in the first place) and the landing page (what first page were they directed to?), and this is extremely valuable information.

By using mod_uid or http_userid_module, the web server itself can generate the user ID and log it even on this very first request. For example, we can easily configure nginx to do this with something like:

userid on;
userid_name brid;
userid_domain 'foo.com';
userid_path /;
userid_expires max;
userid_mark S;

You can then pass it to your Rails application by doing something like this (in nginx, for example):

proxy_set_header X-Nginx-Browser-ID-Got $uid_got;
proxy_set_header X-Nginx-Browser-ID-Set $uid_set;

...and then you'll get data like the following:

request.env['HTTP_X_NGINX_BROWSER_ID_GOT'] # => brid=0100007FE7D7F35241946D1E02030303

or

request.env['HTTP_X_NGINX_BROWSER_ID_SET'] # => brid=0100007FE7D7F35241946D1E02030303

Specifically, you'll get HTTP_X_NGINX_BROWSER_ID_SET (but not _GOT) on the very first request, and then HTTP_X_NGINX_BROWSER_ID_GOT on each subsequent request. You'll also get a cookie — cookies[:brid] — that will look like fwAAAVLz1+cebZRBAwMDAgS=; this is a Base64-encoded, endianness-reversed version of the exact same data.

WebServerUid can help you easily parse the data above, return either form from any input, return a pure-binary version of this data (which is only 16 bytes long — the shortest possible form if you want to store the data in a database or in log files). It also compares correctly (meaning <, >=, ==, !=, eql?, and so on work properly), and hashes correctly (meaning you can store it in a Hash, and find it again, even if searching via a different actual object that is in fact equal to the original key).

Because these UIDs also have internal structure (including the IP address of the server that generated them and the time at which they were generated, among other), WebServerUid also has methods that will return this data for you.

NOTE: You'll probably be happier if you actually term this a browser ID in your application, because that's what it actually is (the implied "user" from uid notwithstanding); these cookies get set uniquely per browser, and never cleared (unless you manually clear them — and then the web server will just re-set them, anyway). Having a per-browser unique ID is an incredibly valuable thing for your analytics, and nicely orthogonal to your concept of user — it's just important that you keep the distinction clear in your mind.

WebServerUid supports:

These are, however, just the versions it's tested against; WebServerUid contains no code that should be at all particularly dependent on exact Ruby versions, and should be compatible with a broad set of versions.

Current build status: Current Build Status

Brought to you by the folks at Swiftype. First version written by Andrew Geweke.

Installation

Add this line to your application's Gemfile:

gem 'web_server_uid'

And then execute:

$ bundle

Or install it yourself as:

$ gem install web_server_uid

Usage

For the most common case — you can parse a WebServerUid out of one of those request.env lines by doing:

uid = WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_GOT'], 'brid')

You can parse it from a cookie by doing:

uid = WebServerUid.from_base64(cookies[:brid])

Generally, you want to try all three sources; you can define a method like this, in your ApplicationController (but make sure the headers and cookie names are correct for your purposes):

def web_server_uid
  [ WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_SET'], 'brid'),
    WebServerUid.from_header(request.env['HTTP_X_NGINX_BROWSER_ID_GOT'], 'brid'),
    WebServerUid.from_base64(cookies[:brid]) ].compact.first
end

(These methods properly just return nil, rather than failing, if passed nil, so this is safe to do.) This will return the first value that's set from the set. (While, theoretically, this will never return nil, you don't want to rely on this, based on experience; automated ping checks, misconfigured front-end servers, and so forth mean that at some point you'll almost certainly get a request that somehow has none of the above set.)

These methods will raise ArgumentError if the data passed in is incorrectly-formatted; it's up to you to decide whether you want to allow this to propagate, or swallow it and proceed without a UID.

Once you have a WebServerUid, you can call these methods on it:

web_server_uid.to_hex_string    # => "0100007FE7D7F35241946D1E02030303"
web_server_uid.to_base64_string # => "fwAAAVLz1+cebZRBAwMDAg=="
web_server_uid.to_binary_string # => "\177\000\000\001R\363\327\347\036m\224A\003\003\003\002"

This should give you pretty much any data you could want to work with these values. Note that there are also class methods from_binary and from_base64 that will accept strings of those formats, too.

All the ordinary comparison operators — <, <=, >, >=, <=>, ==, !=, and eql? — work properly on values of this class; it also hashes properly, so feel free to use it as a hash key.

Contributing

  1. Fork it ( http://github.com/swiftype/web_server_uid/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request