Don't put another dime in the jukebox

published 23 Feb 2010 by Mark Percival
filed under: Code

Or how I stopped writing regular expresssions to parse user agents

Just want the code? Head over the Github and grab it!

Beneath every good mobile app, lies the ability to easily and accurately determine the make and model of the user’s mobile device. And up until a month ago we hard coded this into our apps. But like everything, as time passed, what seemed like the simplest solution began to morph into a complex web of regular expressions and conditional statements.

And we’ve seen a fair number of solutions to the problem. Some clients come with their own complex sets of mobile matching algorithms, while other rely on open source solutions like WURFL. At Squarepush we began to wonder if it was all really necessary.

The problem for the majority

If you’re looking to deliver Java jar files to every Nokia made since 1994 and need a reliable way to determine exactly which phone you’re dealing with, this isn’t the library for you. But according to AdMob, 91% of mobile traffic in North America consist of iPhones, Android, and Blackberry, in that order. So if you’re simply wanting to build the best possible web experience for the majority of your users, something easier is in order

Divining Rod - A magical stick for mobile profiling

When we looked at the current solutions for profiling phones, we noticed a lot of repetition. Most of the phones out there have similar user agents. For example, both the Apple iPhone and the Nexus One have a user agent with the words ‘Apple’, ‘Mobile’, and ‘Safari’.

The other problem was defining capabilities. Both Android and the iPhone are capable of showing videos on YouTube, but the geolocation javascript hooks are slightly different. What we needed was a way to tag multiple mobile devices and not be forced into an absolute hierarchy.

Enter DiviningRod

DiviningRod lets you do something like this:

DiviningRod::Mappings.define do |map|
    map.ua /Apple.*Mobile.*Safari/, :format => :webkit
                                  , :tags => [:webkit, :safari, :youtube_capable] do |webkit|
      webkit.ua /Android/, :name => 'Android', :tags => [:android, :google_gears]
      # Apple iPhone OS
      webkit.with_options :tags => [:apple, :iphone_os] do |iphone|
        iphone.ua /iPad/, :tags => :ipad, :name => 'iPad'
        iphone.ua /iPod/, :tags => :ipod, :name => 'iPod Touch'
        iphone.ua /iPhone/, :tags => :iphone, :name => 'iPhone'
      end
    end

    map.ua /BlackBerry/, :tags => :blackberry, :name => 'BlackBerry'
end

So when we see an Apple iPhone user agent like this:

Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7D11 Safari/528.16

We can go ahead and tag it apple while continuing to look for more specific matches, in this case the iPhone match.

And when we profile that request with DiviningRod, we get to do this:

profile =  DiviningRod::Profile.new(request)

# Notice that the tags aggregate. This is the only hash value that doesn't override.
profile.tags #=> [:webkit, :safari, :youtube_capable, :apple, :iphone_os, :iphone]

# Which we can ask about
profile.iphone? #=> true
profile.apple?  #=> true

# and retrieve arbitrary hash values, like :name, and :format
profile.format  #=> :webkit
profile.name    #=> 'iPhone'

Where we’re headed

Right now it’s a really simple but powerful library, and while we want to keep it as extensible and intuitive as possible, we also want to make it dead simple to get it configured for most applications.

The next step is building out the definitions, and including them in the gem for easier use. Ultimately, we’d like to build one master set of user agent definitions that cover 95% of the mobile apps out there, while still allowing for customization. We think we’re pretty close to that goal.


blog comments powered by Disqus