JavaScript library that gets information about the current user/visitor session. Including location, browser and device details, time zone, last visit time, locale and more.

/**
 * session.js 0.4.1
 * (c) 2012 Iain, CodeJoust
 * session.js is freely distributable under the MIT license.
 * Portions of session.js are inspired or borrowed from Underscore.js, and quirksmode.org demo javascript.
 * This version uses google's jsapi library for location services.
 * For details, see: https://github.com/codejoust/session.js
 */
var session_fetch = (function(win, doc, nav) {
    'use strict';
    // Changing the API Version invalidates olde cookies with previous api version tags.
    var API_VERSION = 0.4;
    // Settings: defaults
    var options = {
        // Use the HTML5 Geolocation API
        // this ONLY returns lat & long, no city/address
        use_html5_location: false,
        // Attempts to use IPInfoDB if provided a valid key
        // Get a key at http://ipinfodb.com/register.php
        ipinfodb_key: false,
        // Leaving true allows for fallback for both
        // the HTML5 location and the IPInfoDB
        gapi_location: true,
        // Name of the location cookie (set blank to disable cookie)
        //   - WARNING: different providers use the same cookie
        //   - if switching providers, remember to use another cookie or provide checks for old cookies
        location_cookie: "location",
        // Location cookie expiration in hours
        location_cookie_timeout: 5,
        // Session expiration in days
        session_timeout: 32,
        // Session cookie name (set blank to disable cookie)
        session_cookie: "first_session",
        get_object: null,
        set_object: null // used for cookie session adaptors
        // if null, will be reset to use cookies by default.
    };

    // Session object
    var SessionRunner = function() {
        win.session = win.session || {};
        // Helper for querying.
        // Usage: session.current_session.referrer_info.hostname.contains(['github.com','news.ycombinator.com'])
        win.session.contains = function(other_str) {
            if (typeof(other_str) === 'string') {
                return (this.indexOf(other_str) !== -1);
            }
            for (var i = 0; i < other_str.length; i++) {
                if (this.indexOf(other_str[i]) !== -1) {
                    return true;
                }
            }
            return false;
        };
        // Merge options
        if (win.session && win.session.options) {
            for (var option in win.session.options) {
                options[option] = win.session.options[option];
            }
        }
        // Modules to run
        // If the module has arguments,
        //   it _needs_ to return a callback function.
        var unloaded_modules = {
            api_version: API_VERSION,
            locale: modules.locale(),
            current_session: modules.session(),
            original_session: modules.session(
                options.session_cookie,
                options.session_timeout * 24 * 60 * 60 * 1000),
            browser: modules.browser(),
            plugins: modules.plugins(),
            time: modules.time(),
            device: modules.device()
        };
        // Location switch
        if (options.use_html5_location) {
            unloaded_modules.location = modules.html5_location();
        } else if (options.ipinfodb_key) {
            unloaded_modules.location = modules.ipinfodb_location(options.ipinfodb_key);
        } else if (options.gapi_location) {
            unloaded_modules.location = modules.gapi_location();
        }
        // Cache win.session.start
        if (win.session && win.session.start) {
            var start = win.session.start;
        }
        // Set up checking, if all modules are ready
        var asynchs = 0,
            module, result,
            check_asynch = function(deinc) {
                if (deinc) {
                    asynchs--;
                }
                if (asynchs === 0) {
                    // Run start calback
                    if (start) {
                        start(win.session);
                    }
                }
            };
        win.session = {};
        // Run asynchronous methods
        for (var name in unloaded_modules) {
            module = unloaded_modules[name];
            if (typeof module === "function") {
                try {
                    module(function(data) {
                        win.session[name] = data;
                        check_asynch(true);
                    });
                    asynchs++;
                } catch (err) {
                    if (win.console && typeof(console.log) === "function") {
                        console.log(err);
                        check_asynch(true);
                    }
                }
            } else {
                win.session[name] = module;
            }
        }
        check_asynch();
    };


    // Browser (and OS) detection
    var browser = {
        detect: function() {
            var ret = {
                browser: this.search(this.data.browser),
                version: this.search(nav.userAgent) || this.search(nav.appVersion),
                os: this.search(this.data.os)
            };
            if (ret.os == 'Linux') {
                var distros = ['CentOS', 'Debian', 'Fedora', 'Gentoo', 'Mandriva', 'Mageia', 'Red Hat', 'Slackware', 'SUSE', 'Turbolinux', 'Ubuntu'];
                for (var i = 0; i < distros.length; i++) {
                    if (nav.userAgent.toLowerCase().match(distros[i].toLowerCase())) {
                        ret.distro = distros[i];
                        break;
                    }
                }
            }
            return ret;
        },
        search: function(data) {
            if (typeof data === "object") {
                // search for string match
                for (var i = 0; i < data.length; i++) {
                    var dataString = data[i].string,
                        dataProp = data[i].prop;
                    this.version_string = data[i].versionSearch || data[i].identity;
                    if (dataString) {
                        if (dataString.indexOf(data[i].subString) != -1) {
                            return data[i].identity;
                        }
                    } else if (dataProp) {
                        return data[i].identity;
                    }
                }
            } else {
                // search for version number
                var index = data.indexOf(this.version_string);
                if (index == -1) return;
                return parseFloat(data.substr(index + this.version_string.length + 1));
            }
        },
        data: {
            browser: [{
                    string: nav.userAgent,
                    subString: "Edge",
                    identity: "Edge"
                },
                {
                    string: nav.userAgent,
                    subString: "Chrome",
                    identity: "Chrome"
                },
                {
                    string: nav.userAgent,
                    subString: "OmniWeb",
                    versionSearch: "OmniWeb/",
                    identity: "OmniWeb"
                },
                {
                    string: nav.vendor,
                    subString: "Apple",
                    identity: "Safari",
                    versionSearch: "Version"
                },
                {
                    prop: win.opera,
                    identity: "Opera",
                    versionSearch: "Version"
                },
                {
                    string: nav.vendor,
                    subString: "iCab",
                    identity: "iCab"
                },
                {
                    string: nav.vendor,
                    subString: "KDE",
                    identity: "Konqueror"
                },
                {
                    string: nav.userAgent,
                    subString: "Firefox",
                    identity: "Firefox"
                },
                {
                    string: nav.vendor,
                    subString: "Camino",
                    identity: "Camino"
                },
                {
                    string: nav.userAgent,
                    subString: "Netscape",
                    identity: "Netscape"
                },
                {
                    string: nav.userAgent,
                    subString: "MSIE",
                    identity: "Explorer",
                    versionSearch: "MSIE"
                },
                {
                    string: nav.userAgent,
                    subString: "Trident",
                    identity: "Explorer",
                    versionSearch: "rv"
                },
                {
                    string: nav.userAgent,
                    subString: "Gecko",
                    identity: "Mozilla",
                    versionSearch: "rv"
                },
                {
                    string: nav.userAgent,
                    subString: "Mozilla",
                    identity: "Netscape",
                    versionSearch: "Mozilla"
                }
            ],
            os: [{
                    string: nav.platform,
                    subString: "Win",
                    identity: "Windows"
                },
                {
                    string: nav.platform,
                    subString: "Mac",
                    identity: "Mac"
                },
                {
                    string: nav.userAgent,
                    subString: "iPhone",
                    identity: "iPhone/iPod"
                },
                {
                    string: nav.userAgent,
                    subString: "iPad",
                    identity: "iPad"
                },
                {
                    string: nav.userAgent,
                    subString: "Android",
                    identity: "Android"
                },
                {
                    string: nav.platform,
                    subString: "Linux",
                    identity: "Linux"
                }
            ]
        }
    };

    var modules = {
        browser: function() {
            return browser.detect();
        },
        time: function() {
            // split date and grab timezone estimation.
            // timezone estimation: http://www.onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/
            var d1 = new Date(),
                d2 = new Date();
            d1.setMonth(0);
            d1.setDate(1);
            d2.setMonth(6);
            d2.setDate(1);
            return ({
                tz_offset: -(new Date().getTimezoneOffset()) / 60,
                observes_dst: (d1.getTimezoneOffset() !== d2.getTimezoneOffset())
            });
            // Gives a browser estimation, not guaranteed to be correct.
        },
        locale: function() {
            var lang = ((
                nav.language ||
                nav.browserLanguage ||
                nav.systemLanguage ||
                nav.userLanguage
            ) || '').split("-");
            if (lang.length == 2) {
                return {
                    country: lang[1].toLowerCase(),
                    lang: lang[0].toLowerCase()
                };
            } else if (lang) {
                return {
                    lang: lang[0].toLowerCase(),
                    country: null
                };
            } else {
                return {
                    lang: null,
                    country: null
                };
            }
        },
        device: function() {
            var device = {
                screen: {
                    width: win.screen.width,
                    height: win.screen.height
                }
            };
            var width, height;
            try {
                width = win.innerWidth || doc.documentElement.clientWidth || doc.body.clientWidth;
            } catch (e) {
                width = 0;
            }
            try {
                height = win.innerHeight || doc.documentElement.clientHeight || doc.body.clientHeight;
            } catch (e) {
                height = 0;
            }
            device.viewport = {
                width: width,
                height: height
            };
            device.is_tablet = !!nav.userAgent.match(/(iPad|SCH-I800|xoom|kindle)/i);
            device.is_phone = !device.is_tablet && !!nav.userAgent.match(/(iPhone|iPod|blackberry|android|htc|lg|midp|mmp|mobile|nokia|opera mini|palm|pocket|psp|sgh|smartphone|symbian|treo mini|Playstation Portable|SonyEricsson|Samsung|MobileExplorer|PalmSource|Benq|Windows Phone|Windows Mobile|IEMobile|Windows CE|Nintendo Wii)/i);
            device.is_mobile = device.is_tablet || device.is_phone;
            return device;
        },
        plugins: function() {
            var check_plugin = function(name) {
                if (nav.plugins) {
                    var plugin, i = 0,
                        length = nav.plugins.length;
                    for (; i < length; i++) {
                        plugin = nav.plugins[i];
                        if (plugin && plugin.name && plugin.name.toLowerCase().indexOf(name) !== -1) {
                            return true;
                        }
                    }
                    return false;
                }
                return false;
            };
            var check_activex_flash = function(versions) {
                var found = true;
                for (var i = 0; i < versions.length; i++) {
                    try {
                        var obj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash" + versions[i]),
                            found = !0;
                    } catch (e) { /* nil */ }
                    if (found) return true;
                }
                return false;
            }
            return {
                flash: check_plugin("flash") || check_activex_flash(['.7', '.6', '']),
                silverlight: check_plugin("silverlight"),
                java: check_plugin("java"),
                quicktime: check_plugin("quicktime")
            };
        },
        session: function(cookie, expires) {
            var session = util.get_obj(cookie);
            if (session == null) {
                session = {
                    visits: 1,
                    start: new Date().getTime(),
                    last_visit: new Date().getTime(),
                    url: win.location.href,
                    path: win.location.pathname,
                    referrer: doc.referrer,
                    referrer_info: util.parse_url(doc.referrer),
                    search: {
                        engine: null,
                        query: null
                    }
                };
                var search_engines = [{
                            name: "Google",
                            host: "google",
                            query: "q"
                        },
                        {
                            name: "Bing",
                            host: "bing.com",
                            query: "q"
                        },
                        {
                            name: "Yahoo",
                            host: "search.yahoo",
                            query: "p"
                        },
                        {
                            name: "AOL",
                            host: "search.aol",
                            query: "q"
                        },
                        {
                            name: "Ask",
                            host: "ask.com",
                            query: "q"
                        },
                        {
                            name: "Baidu",
                            host: "baidu.com",
                            query: "wd"
                        }
                    ],
                    length = search_engines.length,
                    engine, match, i = 0,
                    fallbacks = 'q query term p wd query text'.split(' ');
                for (i = 0; i < length; i++) {
                    engine = search_engines[i];
                    if (session.referrer_info.host.indexOf(engine.host) !== -1) {
                        session.search.engine = engine.name;
                        session.search.query = session.referrer_info.query[engine.query];
                        session.search.terms = session.search.query ? session.search.query.split(" ") : null;
                        break;
                    }
                }
                if (session.search.engine === null && session.referrer_info.search.length > 1) {
                    for (i = 0; i < fallbacks.length; i++) {
                        var terms = session.referrer_info.query[fallbacks[i]];
                        if (terms) {
                            session.search.engine = "Unknown";
                            session.search.query = terms;
                            session.search.terms = terms.split(" ");
                            break;
                        }
                    }
                }
            } else {
                session.prev_visit = session.last_visit;
                session.last_visit = new Date().getTime();
                session.visits++;
                session.time_since_last_visit = session.last_visit - session.prev_visit;
            }
            util.set_obj(cookie, session, expires);
            return session;
        },
        html5_location: function() {
            return function(callback) {
                nav.geolocation.getCurrentPosition(function(pos) {
                    pos.source = 'html5';
                    callback(pos);
                }, function(err) {
                    if (options.gapi_location) {
                        modules.gapi_location()(callback);
                    } else {
                        callback({
                            error: true,
                            source: 'html5'
                        });
                    }
                });
            };
        },
        gapi_location: function() {
            return function(callback) {
                var location = util.get_obj(options.location_cookie);
                if (!location || location.source !== 'google') {
                    win.gloader_ready = function() {
                        if ("google" in win) {
                            if (win.google.loader.ClientLocation) {
                                win.google.loader.ClientLocation.source = "google";
                                callback(win.google.loader.ClientLocation);
                            } else {
                                callback({
                                    error: true,
                                    source: "google"
                                });
                            }
                            util.set_obj(
                                options.location_cookie,
                                win.google.loader.ClientLocation,
                                options.location_cookie_timeout * 60 * 60 * 1000);
                        }
                    };
                    util.embed_script("https://www.google.com/jsapi?callback=gloader_ready");
                } else {
                    callback(location);
                }
            };
        },
        architecture: function() {
            var arch = n.userAgent.match(/x86_64|Win64|WOW64|x86-64|x64\;|AMD64|amd64/) ||
                (n.cpuClass === 'x64') ? 'x64' : 'x86';
            return {
                arch: arch,
                is_x64: arch == 'x64',
                is_x86: arch == 'x68'
            }
        },
        ipinfodb_location: function(api_key) {
            return function(callback) {
                var location_cookie = util.get_obj(options.location_cookie);
                if (!location_cookie && location_cookie.source === 'ipinfodb') {
                    win.ipinfocb = function(data) {
                        if (data.statusCode === "OK") {
                            data.source = "ipinfodb";
                            util.set_obj(
                                options.location_cookie,
                                data,
                                options.location_cookie * 60 * 60 * 1000);
                            callback(data);
                        } else {
                            if (options.gapi_location) {
                                return modules.gapi_location()(callback);
                            } else {
                                callback({
                                    error: true,
                                    source: "ipinfodb",
                                    message: data.statusMessage
                                });
                            }
                        }
                    };
                    util.embed_script("http://api.ipinfodb.com/v3/ip-city/?key=" + api_key + "&format=json&callback=ipinfocb");
                } else {
                    callback(location_cookie);
                }
            }
        }
    };

    // Utilities
    var util = {
        parse_url: function(url_str) {
            var a = doc.createElement("a"),
                query = {};
            a.href = url_str;
            var query_str = a.search.substr(1);
            // Disassemble query string
            if (query_str != '') {
                var pairs = query_str.split("&"),
                    i = 0,
                    length = pairs.length,
                    parts;
                for (; i < length; i++) {
                    parts = pairs[i].split("=");
                    if (parts.length === 2) {
                        query[parts[0]] = decodeURI(parts[1]);
                    }
                }
            }
            return {
                host: a.host,
                path: a.pathname,
                protocol: a.protocol,
                port: a.port === '' ? 80 : a.port,
                search: a.search,
                query: query
            }
        },
        set_cookie: function(cname, value, expires, options) { // from jquery.cookie.js
            if (!cname) {
                return null;
            }
            if (!options) {
                options = {};
            }
            if (value === null || value === undefined) {
                expires = -1;
            }
            if (expires) {
                options.expires = (new Date().getTime()) + expires;
            }
            return (doc.cookie = [
                encodeURIComponent(cname), '=',
                encodeURIComponent(String(value)),
                options.expires ? '; expires=' + new Date(options.expires).toUTCString() : '', // use expires attribute, max-age is not supported by IE
                '; path=' + (options.path ? options.path : '/'),
                options.domain ? '; domain=' + options.domain : '',
                (win.location && win.location.protocol === 'https:') ? '; secure' : ''
            ].join(''));
        },
        get_cookie: function(cookie_name, result) { // from jquery.cookie.js
            return (result = new RegExp('(?:^|; )' + encodeURIComponent(cookie_name) + '=([^;]*)').exec(doc.cookie)) ? decodeURIComponent(result[1]) : null;
        },
        embed_script: function(url) {
            var element = doc.createElement("script");
            element.type = "text/javascript";
            element.src = url;
            (doc.body || doc.getElementsByTagName("body")[0] || doc.head).appendChild(element);
        },
        package_obj: function(obj) {
            if (obj) {
                obj.version = API_VERSION;
                var ret = JSON.stringify(obj);
                delete obj.version;
                return ret;
            }
        },
        set_obj: function(cname, value, expires, options) {
            util.set_cookie(cname, util.package_obj(value), expires, options);
        },
        get_obj: function(cookie_name) {
            var obj;
            try {
                obj = JSON.parse(util.get_cookie(cookie_name));
            } catch (e) {}
            if (obj && obj.version == API_VERSION) {
                delete obj.version;
                return obj;
            }
        }
    };

    // cookie options override
    if (options.get_object != null) {
        util.get_obj = options['get_object'];
    }
    if (options.set_object != null) {
        util.set_obj = options['set_object'];
    }

    // JSON
    var JSON = {
        parse: (win.JSON && win.JSON.parse) || function(data) {
            if (typeof data !== "string" || !data) {
                return null;
            }
            return (new Function("return " + data))();
        },
        stringify: (win.JSON && win.JSON.stringify) || function(object) {
            var type = typeof object;
            if (type !== "object" || object === null) {
                if (type === "string") {
                    return '"' + object + '"';
                }
            } else {
                var k, v, json = [],
                    isArray = (object && object.constructor === Array);
                for (k in object) {
                    v = object[k];
                    type = typeof v;
                    if (type === "string")
                        v = '"' + v + '"';
                    else if (type === "object" && v !== null)
                        v = this.stringify(v);
                    json.push((isArray ? "" : '"' + k + '":') + v);
                }
                return (isArray ? "[" : "{") + json.join(",") + (isArray ? "]" : "}");
            }
        }
    };

    // Initialize SessionRunner
    SessionRunner();

});
// Switch for testing purposes.
if (typeof(window.exports) === 'undefined') {
    session_fetch(window, document, navigator);
} else {
    window.exports.session = session_fetch;
}