Skip to content
Zwetan Kjukov edited this page Aug 23, 2015 · 9 revisions

The simplest possible Tracker

In the past, we had people complaining about the size of a library of 30KB (which is completely ridiculous IMHO), and so to prevent such complains or to give you an overview of the minimun needed to send data to Google Analytics servers, we will describe a step by step here.

The Measurement Protocol

As you can see in the Measurement Protocol Developer Guide, to send data to Google Analytics servers you need 3 things:

  • a client ID
  • a well formatted payload
  • an HTTP POST or GET

The Client ID

As explained in Required Values For All Hits, you need 4 required values each time you send a request, all the rest is optional, but if one of those 4 is missing your request will fail.

The protocol version is a piece of cake, just use v=1.

The Tracking ID is quite easy too, you get it from the Google Analytics panel, something following this format UA-XXXX-Y.

The Hit Type is one of the following strings: pageview, screenview, event, etc. (documented here Hit Type).

So, the real hard parameter to manage is the Client ID (again documented here Client ID).

The problem is not really to generate that client ID but to save it and reuse it.

If for example you generate a new Client ID for each requests, the Google Analytics servers will see a different user for each request, something you really don't want.

Another example would be tracking the same user from 2 different environments: on a web page the user would be tracked by analytics.js and within that page a SWF file using analytics.swc would track also that user.

In that case, if you use 2 different Client ID, you can not follow the flow of the user browsing.

At the opposite, if you re-use the same Client ID, you can see where and when the user navigate from the HTML to the SWF (for example) etc.

If in an HTML environment you would use Cookies, in a Flash environment we advise you to use the SharedObject class

var so:SharedObject = SharedObject.getLocal( "_ga" );
var cid:String;

if( !_so.data.clientid )
{
    cid = generateUUID(); //not found so we generate it
    so.data.clientid = cid;   //then we save it
    so.flush( 1024 );
}
else
{
    cid = so.data.clientid; //found so we reuse it
}

For the Client ID generation, the best method we know of is the implementation you can find here google.analytics.utils.generateUUID().

Why best ?

That said, you could use different way to generate this Client ID.

Basically, it just need to be a random sequence to anonymously identify a particular user, device or browser instance, we could then reuse what we were doing for gaforflash:

function generate32bitRandom():int
{
    return Math.round( Math.random() * 0x7fffffff );
}

function generateHash( input:String ):int
{
    var hash:int      = 1;
    var leftMost7:int = 0;
    var pos:int;
    var current:int;
    
    if(input != null && input != "")
    {
        hash = 0;
        
        for( pos = input.length - 1 ; pos >= 0 ; pos-- )
        {
            current   = input.charCodeAt(pos);
            hash      = ((hash << 6) & 0xfffffff) + current + (current << 14);
            leftMost7 = hash & 0xfe00000;
            
            if(leftMost7 != 0)
            {
                hash ^= leftMost7 >> 21;
            }
        }
    }
    
    return hash;
}

function generateUserDataHash():Number
{
    var data:String = "";
        data += Capabilities.cpuArchitecture;
        data += Capabilities.language;
        data += Capabilities.manufacturer;
        data += Capabilities.os;
        data += Capabilities.screenColor;
        data += Capabilities.screenDPI;
        data += Capabilities.screenResolutionX + "x" + Capabilities.screenResolutionY;

    return generateHash( data );
}

function getClientID():Number
{
    var cid:Number = (generate32bitRandom() ^ generateUserDataHash()) * 0x7fffffff;
    return cid;
}

Well Formatted Payload Data

It is all about following the rules of the Measurement Protocol Parameter Reference but most importantly how to encode them correctly.

It is described here URL Encoding Values with the following

All values sent to Google Analytics must be both UTF-8 and URL Encoded.
To send the key dp with the value /my page €, you will first need to make sure this is UTF-8 encoded, then url encoded, resulting in the final string:
dp=%2Fmy%20page%20%E2%82%AC
If any of the characters are encoded incorrectly, they will be replaced with the unicode replacement character xFFFD.

And then come ECMAScript history and ActionScript 3.0 ...

In AS3 you have basically 3 different way to URL Encode stuff

  • the escape() function
    Converts the parameter to a string and encodes it in a URL-encoded format, where most nonalphanumeric characters are replaced with % hexadecimal sequences.
  • the encodeURI() function
    Encodes a string into a valid URI (Uniform Resource Identifier).
    Converts a complete URI into a string in which all characters are encoded as UTF-8 escape sequences unless a character belongs to a small group of basic characters.
  • the encodeURIComponent() function
    Encodes a string into a valid URI component.
    Converts a substring of a URI into a string in which all characters are encoded as UTF-8 escape sequences unless a character belongs to a very small group of basic characters.

and the nuance between them is subtle ...

To keep a long story short, you really need to follow RFC 2396, read a bit O'Reilly's "HTML: The Definitive Guide" (page 164), and then you understand you need to use encodeURIComponent().

For example, you would do that

var payload:String = "";
    payload += "dp=" + encodeURIComponent( "/my page €" );

A longer example would be

var trackingID:String = "UA-123456-0";
var clientID:String   = generateUUID();

var payload:Array = [];
    payload.push( "v=1" );
    payload.push( "tid=" + trackingID );
    payload.push( "cid=" + clientID );
    payload.push( "t=pageview" );
    payload.push( "dh=mydomain.com" );
    payload.push( "dp=" + encodeURIComponent( "/my page €" ) );
    payload.push( "dt=" + encodeURIComponent( "My Page Title with €" ) );

var data:String = payload.join( "&" );

An HTTP POST or GET

In ActionScript 3.0 this will involve using a URLRequest with a Loader or a URLLoader.

To keep things short and compatible everywhere I would advise to use a Loader class (as with the URLLoader you can encounter security restriction) and send a GET request.

for example

var request:URLRequest = new URLRequest();
    request.method = URLRequestMethod.GET;
    request.url = "http://www.google-analytics.com/collect";
    request.data = payload;

var loader:Loader = new Loader();
    loader.load( request );

I will not go more into the details here (as this could require its own page to explain it all), just know that this HTTP request can be done in a lot of different way and the library is made in such a way you can easily change it or even provide your own implementation.

The Final Result

A single class with a lot of traces that illustrate everything said above.

There is a good reason if this class is not part of the source code, we don't plan to support it at all.

Use it for testing, use it to understand how the Measurement protocol works, but then use it at your own risk.

This class also illustrates why we do prefer to have a library instead, so we can have structure, tests, reusable code, etc.

SimplestTracker.as

package test
{
    import flash.crypto.generateRandomBytes;
    import flash.display.Loader;
    import flash.events.ErrorEvent;
    import flash.events.Event;
    import flash.events.HTTPStatusEvent;
    import flash.events.IOErrorEvent;
    import flash.events.NetStatusEvent;
    import flash.events.UncaughtErrorEvent;
    import flash.net.SharedObject;
    import flash.net.SharedObjectFlushStatus;
    import flash.net.URLRequest;
    import flash.net.URLRequestMethod;
    import flash.utils.ByteArray;

    public class SimplestTracker
    {
        private var _so:SharedObject;
        private var _loader:Loader;
        
        public var trackingID:String;
        public var clientID:String;
        
        public function SimplestTracker( trackingID:String )
        {
            trace( "SimplestTracker starts" );
            this.trackingID = trackingID;
            trace( "trackingID = " + trackingID );
            
            trace( "obtain the Client ID" );
            this.clientID  = _getClientID();
            trace( "clientID = " + clientID );
        }
        
        private function onFlushStatus( event:NetStatusEvent ):void
        {
            _so.removeEventListener( NetStatusEvent.NET_STATUS, onFlushStatus);
            trace( "User closed permission dialog..." );
            
            switch( event.info.code )
            {
                case "SharedObject.Flush.Success":
                trace( "User granted permission, value saved" );
                break;
                
                case "SharedObject.Flush.Failed":
                trace( "User denied permission, value not saved" );
                break;
            }
        }
        
        private function onLoaderUncaughtError( event:UncaughtErrorEvent ):void
        {
            trace( "onLoaderUncaughtError()" );
            
            if( event.error is Error )
            {
                var error:Error = event.error as Error;
                trace( "Error: " + error );
            }
            else if( event.error is ErrorEvent )
            {
                var errorEvent:ErrorEvent = event.error as ErrorEvent;
                trace( "ErrorEvent: " + errorEvent );
            }
            else
            {
                trace( "a non-Error, non-ErrorEvent type was thrown and uncaught" );
            }
            
            _removeLoaderEventsHook();
        }
        
        private function onLoaderHTTPStatus( event:HTTPStatusEvent ):void
        {
            trace( "onLoaderHTTPStatus()" );
            trace( "status: " + event.status );
            
            if( event.status == 200 )
            {
                trace( "the request was accepted" );
            }
            else
            {
                trace( "the request was not accepted" );
            }
        }
        
        private function onLoaderIOError( event:IOErrorEvent ):void
        {
            trace( "onLoaderIOError()" );
            _removeLoaderEventsHook();
        }
        
        private function onLoaderComplete( event:Event ):void
        {
            trace( "onLoaderComplete()" );
            
            trace( "done" );
            _removeLoaderEventsHook();
        }
        
        private function _removeLoaderEventsHook():void
        {
            _loader.uncaughtErrorEvents.removeEventListener( UncaughtErrorEvent.UNCAUGHT_ERROR, onLoaderUncaughtError );
            _loader.contentLoaderInfo.removeEventListener( HTTPStatusEvent.HTTP_STATUS, onLoaderHTTPStatus );
            _loader.contentLoaderInfo.removeEventListener( Event.COMPLETE, onLoaderComplete );
            _loader.contentLoaderInfo.removeEventListener( IOErrorEvent.IO_ERROR, onLoaderIOError );
        }
        
        private function _generateUUID():String
        {
           var randomBytes:ByteArray = generateRandomBytes( 16 );
            randomBytes[6] &= 0x0f; /* clear version */
            randomBytes[6] |= 0x40; /* set to version 4 */
            randomBytes[8] &= 0x3f; /* clear variant */
            randomBytes[8] |= 0x80; /* set to IETF variant */
            
            var toHex:Function = function( n:uint ):String
            {
                var h:String = n.toString( 16 );
                h = (h.length > 1 ) ? h: "0"+h;
                return h;
            }
            
            var str:String = "";
            var i:uint;
            var l:uint = randomBytes.length;
            randomBytes.position = 0;
            var byte:uint;
            
            for( i=0; i<l; i++ )
            {
                byte = randomBytes[ i ];
                str += toHex( byte );
            }
            
            var uuid:String = "";
            uuid += str.substr( 0, 8 );
            uuid += "-";
            uuid += str.substr( 8, 4 );
            uuid += "-";
            uuid += str.substr( 12, 4 );
            uuid += "-";
            uuid += str.substr( 16, 4 );
            uuid += "-";
            uuid += str.substr( 20, 12 );
            
            return uuid;
        }
        
        private function _getClientID():String
        {
            trace( "Load the SharedObject '_ga'" );
            _so = SharedObject.getLocal( "_ga" );
            var cid:String;
            
            if( !_so.data.clientid )
            {
                trace( "CID not found, generate Client ID" );
                cid = _generateUUID();
                
                trace( "Save CID into SharedObject" );
                _so.data.clientid = cid;
                
                var flushStatus:String = null;
                try
                {
                    flushStatus = _so.flush( 1024 ); //1KB
                }
                catch( e:Error )
                {
                    trace( "Could not write SharedObject to disk: " + e.message );
                }
                
                if( flushStatus != null )
                {
                    switch( flushStatus )
                    {
                        case SharedObjectFlushStatus.PENDING:
                        trace( "Requesting permission to save object..." );
                        _so.addEventListener( NetStatusEvent.NET_STATUS, onFlushStatus);
                        break;
                        
                        case SharedObjectFlushStatus.FLUSHED:
                        trace( "Value flushed to disk" );
                        break;
                    }
                }
                
            }
            else
            {
                trace( "CID found, restore from SharedObject" );
                cid = _so.data.clientid;
            }
            
            return cid;
        }
        
        public function sendPageview( page:String, title:String = "" ):void
        {
            trace( "sendPageview()" );
            
            var payload:Array = [];
                payload.push( "v=1" );
                payload.push( "tid=" + trackingID );
                payload.push( "cid=" + clientID );
                payload.push( "t=pageview" );
                /*payload.push( "dh=mydomain.com" ); */
                payload.push( "dp=" + encodeURIComponent( page ) );
                
            if( title && (title.length > 0) )
            {
                payload.push( "dt=" + encodeURIComponent( title ) );    
            }
            
            var url:String = "";
                url += "http://www.google-analytics.com/collect";

            var request:URLRequest = new URLRequest();
                request.method = URLRequestMethod.GET;
                request.url    = url;
                request.data   = payload.join( "&" );

            trace( "request is: " + request.url + "?" + request.data );
                
            _loader = new Loader();
            _loader.uncaughtErrorEvents.addEventListener( UncaughtErrorEvent.UNCAUGHT_ERROR, onLoaderUncaughtError );
            _loader.contentLoaderInfo.addEventListener( HTTPStatusEvent.HTTP_STATUS, onLoaderHTTPStatus );
            _loader.contentLoaderInfo.addEventListener( Event.COMPLETE, onLoaderComplete );
            _loader.contentLoaderInfo.addEventListener( IOErrorEvent.IO_ERROR, onLoaderIOError );
            
            try
            {
                trace( "Loader send request" );
                _loader.load( request );
            }
            catch( e:Error )
            {
                trace( "unable to load requested page: " + e.message );
                _removeLoaderEventsHook();
            }
            
        }
    }
}

usage:

var tracker:SimplestTracker = new SimplestTracker( "UA-12345678-0" );
    tracker.sendPageview( "/my page €", "My Page Title with €" );

This should work everywhere

  • with a SWF running on localhost
  • with a SWF running on your own domain
  • with an AIR application (either Desktop or Mobile)

(it should, if it doesn't don't come asking for help "why it does not work ?", please use the library instead)

you should get an output similar to that:

SimplestTracker starts
trackingID = UA-12345678-0
obtain the Client ID
Load the SharedObject '_ga'
CID found, restore from SharedObject
clientID = 35009a79-1a05-49d7-b876-2b884d0f825b
sendPageview()
request is: http://www.google-analytics.com/collect?v=1&tid=UA-12345678-0&cid=35009a79-1a05-49d7-b876-> 2b884d0f825b&t=pageview&dp=%2Fmy%20page%20%E2%82%AC&dt=My%20Page%20Title%20with%20%E2%82%AC
Loader send request
onLoaderHTTPStatus()
status: 200
the request was accepted
onLoaderComplete()
done

Conclusion

What we have described above is the strict minimum to "get it to work", it is not something we plan to support or encourage.

That said, if you are a developer or just interested in how it works "internally", that's about it and pretty simple to follow (I hope).

There are no secrets, well... not really, Google folks made a tremendous work simplifying and documenting the protocol, but we still thought this needed a "stronger" library and you'll be basically the judge of that: use the "script" above modify it, rename it etc. if you think it does the job or use our library if it suits you better, your choice :).