Custom link click tracking using Omniture

Omniture Logo

Omniture is the de facto standard tracking and analytics system that most online retailers use. It has a suite of reporting metrics and allows for custom reporting variables. It is primarily designed for online stores to track usage, conversions and sales.

Admittedly, I am not that big a fan of Omniture. Their SiteCatalyst reporting application is slower than Google Analytics and not as flexible as it could be. My biggest gripe is their tracking JavaScript code, it’s just plain terrible. It’s slow, obfuscated (really? why do this?), bloated, impossible to debug and not built using modern practices. Oh, and they’re also insanely expensive if you want to get at all fancy.

This article is simply a straight how-to for building a custom link tracking JavaScript in the hopes that I can save some other JavaScript developer out there the headaches and tears usually associated with learning advanced Omniture implementations.

Omniture basics

The Omniture JavaScript does all of its work through the s object. This object has many different methods and properties that are all ingeniously designed to confuse and torment would-be implementers.

For custom tracking metrics you will be using prop codes, which are called “Custom Insights” in the SiteCatalyst reporting software. You associate a specific prop code to a specific piece of data. So you may associate s.prop1 to be the category of a product and s.prop2 to be the name of the product.

The way we track is by loading up prop variables with strings then telling the Omniture code to run. This is done by calling s.t() for a pageview track or s.tl() for a link click track.

How to track a click action with custom prop data

The code to track an action is actually quite simple, loading prop values and calling the tracking method. Here is what a tracking script looks like. This example is the basic custom click tracking code that you would expect to see in an onclick event attached to an anchor tag.

s.linkTrackVars = 'prop1,prop2';
s.linkTrackEvents = 'None';
s.prop1 = 'Televisions';
s.prop2 = 'Samsung - LN40B530 - 40" LCD TV - 1080p (FullHD)';
s.tl(this, 'o');

Now, this is fairly self-explanatory, but I’ll break this down line for line so you know exactly what’s happening and what these statement mean.

  • s.linkTrackVars declares which prop codes you will be using in this event. This property is a comma-separated list of each prop code that you want tracked.
  • s.linkTrackEvents are the “events” that an action may represent. Usually you will use events like “purchase” or “save” but you can put just about anything you want in here. It’s just a different way of looking at the same information. I typically set it to none and use custom insights for reports.
  • s.prop1 and s.prop2 are the custom metrics that will be attached to this click event. In this case prop1 is the product category and prop2 is the product name.
  • s.tl(this, ‘o’) calls the tracking action. The tl() method (a.k.a. the trackLinks method) requires two parameters, this anchor object to be tracked (should always be this) and the type of link we are tracking. There are three types of basic links that is method understands: o is the “Other/General” link type, d is a “File Download” link, and e which is an “Exit Link”. Anything that isn’t a download or exit link should be given the o tag.

There you have it, not incredibly complex, but not obviously simple either. There are a bunch of other properties that you can attach to a trackLinks event, but this is what 99% of them look like.

Building a script to attach link tracking events automatically

There are two standard ways to attach these custom link tracking events to the anchors that you want to track: the stupid method, and the smart method.

  • The stupid method is the most common. This involves adding an onclick tag to every anchor and filling it with the appropriate code.
    • Pros: Stupid, simple, always works
    • Cons: Stupid, unmaintainable, ugly, increases markup, adds non-semantic markup
  • The smart method is to build a single script that will attach the code to every link you want to track automatically on page load.
    • Pros: Smart, not-stupid, simple, maintainable
    • Cons: Links don’t get the tracking events attached until the whole page has finished loading.

For this rest of this article I will assume that you want to use the smart method. Basically, we will build a script that will look at every single anchor tag in the DOM and attach the click tracking events to any links matching whatever criteria you choose. This script will automatically run when the page loads.

Defining the criteria and search pattern

First off, you must be able to identify the links that you want to track. I have used two different systems for this: Searching href tags and adding rel tags.

Searching href tags is generally the best option. This means matching the link href and attaching the event based on that data. However I have found situations where I didn’t want to track every link that had the string “products.aspx” in it, so I chose to add rel tags to those links that I did want to track. This way I could just search for links that had rel=”product” in them.

The technique is still basically the same, we will gather all of the anchors in the document into an array, then iterate through the array, attaching the appropriate events and data to those anchor which meet our criteria for each type of page.

The basic script structure

No matter what you choose as your search criteria and trackable data you will need a script that has at least the following features:

  • Iterates through all anchors on a page
  • Allows you to attach tracking events without overriding any existing events
  • Automatically runs as soon as the page loads
  • Highly optimized for minimum execution time
  • Cannot throw an error that will break the page
  • Unobtrusive design that doesn’t have any dependencies (other than Omniture)
  • Namespaced in object literal notation so that it can never have any confilcts

The basic JavaScript object that I use for this

// Omniture click tracking
// http://stevenbenner.com/2010/03/custom-link-click-tracking-using-omniture/

if(!sb_trackLinks) { var sb_trackLinks = new Object(); };

sb_trackLinks = {
	// initialize the click-tracking system and attach events
	init: function() {
		// verify that we are running in a modern browser, if not give up silently
		if (!document.getElementsByTagName) return false;

		// iterate through all of the links in the document
		var anchors = document.getElementsByTagName('a');
		for (var i=anchors.length-1; i>=0; i--) {

			// SEARCH STATEMENTS AND TRACKING VARIABLE BUILDING GOES HERE

		};
	},

	// tracking function
	track: function(obj, propCodes, linkType = 'o') {
		s = s_gi(s_account);
		// reset tracking properties
		s.linkTrackVars = '';
		s.linkTrackEvents = 'None';
		// iterate through each property in propCodes
		for (code in propCodes) {
			s.linkTrackVars += code + ','; // build the CSV list
			s1 = propCodes1;     // attach the prop code
		}
		// track it
		s.tl(obj, linkType);
	},

	// non-breaking/non-overriding/cross-browser event attachment
	addEvent: function(obj, evType, fn, useCapture) {
		if (obj.addEventListener) {
			obj.addEventListener(evType, fn, useCapture);
			return true;
		} else if (obj.attachEvent) {
			var r = obj.attachEvent('on' + evType, fn);
			return r;
		} else {
			obj['on' + evType] = fn;
		};
	}
};

// auto-init on load
sb_trackLinks.addEvent(window, 'load', sb_trackLinks.init);

This is the basic outline of my JavaScript click tracking object. It has three methods and automatically runs itself in the window.onload event.

I've put a lot of thought into optimizing this script. For instance, notice that I reverse iterate through the anchors array. This technique has been proven to be faster because the array length is only calculated once when the for statement begins. If you do the while i<array.length style then the length is calculated once for every iteration in the loop.

When you build any kind of tracking or analytics JavaScript your highest priority should be to shave every nanosecond from it's operations. It is acceptable to sacrifice readability for performance here. After all, as a web developer your primary job should be to improve user experience, tracking scripts are a necessity but do not benefit users in any way, so you do your best to reduce their impact on user experience.

The sb_trackLinks methods

There are three methods in the sb_trackLinks object:

init()
The init() method is where the startup, initialization and event attachment will occur. We still need to build the search criteria, tracking variables and event attachment code before this will do anything.
track(obj, propCodes, linkType)
The track() method is the code that will be attached to the onclick events for the anchors that we want to track. It requires two parameters and will accept a third.

  • obj: The first parameter is the anchor object that we will be tracking, in the context of our for statement in the init() method this will always be anchors[i].
  • propCodes: The second parameter is an object with the prop codes that we want to use. This should be a standard JavaScript object with the conventions as the s.propXX codes. So if you want to track prop1 you should set propCodes['prop1'] to the value that you want and pass it through to the track() method.
  • linkType: The third parameter is the link type that will be passed to s.tl() and is optional. It will default to 'o'. As I said earlier, it will accept o, d and e as valid values.

You may have to modify this function if your Omniture code has been customized.

addEvent(obj, evType, fn, useCapture)
The addEvent() method is the standard addEvent that you see everywhere on the internet.

Crafting the search and tracking variables

Now that we have a basic structure that we can use, all that we need to build is the anchor search statements, tracking variable and event attachments.

This needs to be custom built for your specific needs. The application, URL structure and tracking needs are all critical parts of this equation. If you are using friendly URLs and need to parse data from them then you will may have to deconstruct the URI to grab the specific data that you want to track in prop codes. If you need data from somewhere else on the page you will have to figure out out to grab it. Etcetera.

I'll provide you with an example based on our previous examples. In this case we are tracking clicks in an online store application. We are using standard query string parameters in our URLs and do not need any other special data.

This is what our for statement will look like for this site:

for (var i=anchors.length-1; i>=0; i--) {
	// parse the query string into an object
	var queryString = {};
	anchors[i].href.replace(
		new RegExp("([^?=&]+)(=([^&]*))?", "g"),
		function($0, $1, $2, $3) { queryString[$1] = $3; }
	);

	// conditionals to match link href targets
	if (anchors[i].href == 'http://your.domain/' || anchors[i].href.indexOf('home.aspx') > -1) {
		// home page
		sb_trackLinks.addEvent(anchors[i], 'click', (function() {
				return function() {
					try {
						sb_trackLinks.track(
							this,
							{
								'prop1': 'home'
							}
						);
					} catch(e) {}
					return true;
				};
			})()
		);
	} else if (anchors[i].href.indexOf('category.aspx') > -1) {
		// category pages
		sb_trackLinks.addEvent(anchors[i], 'click', (function(catId) {
				return function() {
					try {
						sb_trackLinks.track(
							this,
							{
								'prop1': 'category',
								'prop2': catId
							}
						);
					} catch(e) {}
					return true;
				};
			})(queryString['category_id'])
		);
	} else if (anchors[i].href.indexOf('product.aspx') > -1) {
		// product pages
		sb_trackLinks.addEvent(anchors[i], 'click', (function(catId, productId, productName) {
				return function() {
					try {
						sb_trackLinks.track(
							this,
							{
								'prop1': 'product',
								'prop2': catId,
								'prop3': productId,
								'prop4': productName
							}
						);
					} catch(e) {}
					return true;
				};
			})(queryString['category_id'], queryString['product_id'], queryString['product_name'])
		);
	};
};

This is a bit technical, and uses some advanced JavaScript techniques. However, it should be fairly understandable if you know your way around JavaScript.

Since we need to grab query string values for most of our tracked pages I start off by parsing the query string into an object.

I then proceed down a list of if statements that try to match a specific page. Inside each if statement I attach an onclick event to the current anchor. That event passes our current data down through the JavaScript scope via a (function(v){})(data) statement. Inside that event I call the track() method with the data and return true so that the browser follows the link once the code as been executed.

This can get very long indeed if you have a lot of different types of pages in your web application that need click tracking. As always, order your if statements by commonality, putting the most commonly matched statements first. The script engine will have to fail every if check to get to the last statement. This minor optimization may save you a millisecond or two.

It is very important that you wrap all of the code inside the event in a try{}catch(e){} just in case something somewhere breaks. If you do not do this then the links on your site will stop working if anything goes wrong. There is nothing quite as asinine as links not working because the tracking script is broken.

Putting it all together

Now that we've built the search and attach code all we have to do is insert it into our base script. The result looks like this:

// Omniture click tracking
// http://stevenbenner.com/2010/03/custom-link-click-tracking-using-omniture/

if(!sb_trackLinks) { var sb_trackLinks = new Object(); };

sb_trackLinks = {
	// initialize the click-tracking system and attach events
	init: function() {
		// verify that we are running in a modern browser, if not give up silently
		if (!document.getElementsByTagName) return false;

		// iterate through all of the links in the document
		var anchors = document.getElementsByTagName('a');
		for (var i=anchors.length-1; i>=0; i--) {
			// parse the query string into an object
			var queryString = {};
			anchors[i].href.replace(
				new RegExp("([^?=&]+)(=([^&]*))?", "g"),
				function($0, $1, $2, $3) { queryString[$1] = $3; }
			);

			// conditionals to match link href targets
			if (anchors[i].href == 'http://your.domain/' || anchors[i].href.indexOf('home.aspx') > -1) {
				// home page
				sb_trackLinks.addEvent(anchors[i], 'click', (function() {
						return function() {
							try {
								sb_trackLinks.track(
									this,
									{
										'prop1': 'home'
									}
								);
							} catch(e) {}
							return true;
						};
					})()
				);
			} else if (anchors[i].href.indexOf('category.aspx') > -1) {
				// category pages
				sb_trackLinks.addEvent(anchors[i], 'click', (function(catId) {
						return function() {
							try {
								sb_trackLinks.track(
									this,
									{
										'prop1': 'category',
										'prop2': catId
									}
								);
							} catch(e) {}
							return true;
						};
					})(queryString['category_id'])
				);
			} else if (anchors[i].href.indexOf('product.aspx') > -1) {
				// product pages
				sb_trackLinks.addEvent(anchors[i], 'click', (function(catId, productId, productName) {
						return function() {
							try {
								sb_trackLinks.track(
									this,
									{
										'prop1': 'product',
										'prop2': catId,
										'prop3': productId,
										'prop4': productName
									}
								);
							} catch(e) {}
							return true;
						};
					})(queryString['category_id'], queryString['product_id'], queryString['product_name'])
				);
			};
		};
	},

	// tracking function
	track: function(obj, propCodes, linkType = 'o') {
		s = s_gi(s_account);
		// reset tracking properties
		s.linkTrackVars = '';
		s.linkTrackEvents = 'None';
		// iterate through each property in propCodes
		for (code in propCodes) {
			s.linkTrackVars += code + ','; // build the CSV list
			s1 = propCodes1;     // attach the prop code
		}
		// track it
		s.tl(obj, linkType);
	},

	// non-breaking/non-overriding/cross-browser event attachment
	addEvent: function(obj, evType, fn, useCapture) {
		if (obj.addEventListener) {
			obj.addEventListener(evType, fn, useCapture);
			return true;
		} else if (obj.attachEvent) {
			var r = obj.attachEvent('on' + evType, fn);
			return r;
		} else {
			obj['on' + evType] = fn;
		};
	}
};

// auto-init on load
sb_trackLinks.addEvent(window, 'load', sb_trackLinks.init);

Save this code in the s_code.js (or whatever you named it) script for your Omniture-enabled web site and it will start sending click data back to your Omniture account.

Gotchas

There are some possible problems that may crop up using this system.

  • Page never stops loading
    I have seen pages that simply never stop loading because of poor use of AJAX, slow servers, or broken dependencies. The click tracking events will not be attached to links until the loading icon in the browser stops.
  • Links added after page load
    If there are any links that are created after the page load event is fired, such as AJAX widgets or dynamic links, then they will not exist when this script is run and therefor will not have any tracking events attached to them.
  • Omniture conflicts
    There have been several occasions where I've seen people try to use two (or more) instances of Omniture on the same page. If you have to deal with this then you need to modify the track() method to use your account name.
  • 500ms link delay
    The s.tl() function has a half-second delay built into it. This is because the tracking data is sent by requesting a 1 pixel web beacon from the Omniture servers and the script needs to give the browser enough time to send the request. Unfortunately, this also adds an annoying delay to every link that you attach tracking code to.

Advanced tracking (more than just links)

Omniture link tracking can be used for more than just links. You can use the s.tl() method to send any data at any time for any reason. Do you want to track when someone puts their mouse over an image, or how long they waited before scrolling, or even what text they selected? You can do all of that, and more if you really want to.

If you ask me those fine-grain viewer eyeball focus type of reports are not only invasive but genuinely useless. However, I've been asked to do even worse invasive and pointless tracking before. If you need to you can send any data you want on any DOM event, or even with timers.

The only thing that you must overcome is that the object you pass to s.tl() must be an anchor, or more specifically, something with an href attribute. There is nothing about this in the documentation, but it simply doesn't seem to work unless it has an anchor with an href passed to it. You can get around this by creating an element or simply passing true.

This is, however, the subject of a different article.

Conclusion

As you can see it's not that difficult to implement custom link tracking with Omniture and you can get some very powerful reports without that much work. I still cannot give my glowing stamp of approval for Omniture as a reporting solution, for various reasons, but if you are using it then hopefully this tutorial will save you some time and headaches.

If you have any questions or comments please leave them in the form below and I will do my best to answer them. You can also take a look at the Custom Link Tracking post in the Omniture blog. It's a year old, but the author is still responding to questions.

By:
Updated: Apr 13th, 2010

Comments

  1. Steven,

    I hope you don’t mind me jumping in here; if you do, please feel free to delete this comment. :)

    Great technical post — thanks for sharing. Hopefully it will help some of your readers implement link tracking more effectively. Thanks for sharing your insights (and for linking back to my higher-level post on the topic from last year); this is an extremely useful technique for capturing just about anything that isn’t considered a Page View.

    I’m not sure whether you’re a frequent SiteCatalyst user or not, but I would like to respond to a few of the charges you’ve levied against the product.

    > Their SiteCatalyst reporting application is
    > woefully slow and inept.

    While I haven’t found performance issues to be extremely widespread during my four years working with and supporting the tool, we’ve actually made a number of platform improvements over the past year that improve performance significantly. Massive reports (large date range, many metrics, tons of unique values, etc.) may return more slowly than others, but examples of this should be few and far between. Additionally, if users are experiencing reporting slowness, there absolutely ARE things that we can look into.

    > If you want any kind of custom collation or
    > correlation report you have to call them and
    > have them custom craft the report for you.

    I don’t believe this has been true in at least a few years. SiteCatalyst admins can set up their own correlations (as well as managing the majority of other settings) directly within the tool. At the same time, if you’ve run into features that you cannot enable/manage within the Admin Console, please let me know and I’ll at least find out why the given feature is not managed there.

    > But my biggest gripe is their tracking JavaScript
    > code, it’s just plain terrible. It’s slow,
    > obfuscated (really? why do this?), bloated,
    > impossible to debug and not built using modern
    > practices.

    A few things. First, note that we’ve released a few “alternative” data collection methods (PHP, Java, XML Data Insertion) over the past couple of years if you truly hate our JavaScript. Personally, the Data Insertion (XML) API is my favorite. Second, would you be open to further discussion on specific points of complaint (at the code level) regarding our JavaScript? We do have a team of qualified engineers who work on updating the functionality contained there. Who knows — your feedback may prove valuable in resolving some of the issues with the code that bother you. We’re always open to feedback and have an excellent track record of applying these requests into our code.

    Feel free to reach out to me directly at omniturecare at adobe dot com if you’d like to discuss further!

    Thanks,
    Ben Gaines
    Community Manager
    Omniture, an Adobe company

  2. Hi Ben! Thanks for checking out my article. It wasn’t my intention to levy charges against Omniture in this post, but I can see why some of my quips could be taken that way.

    I’ve only been using Omniture for about 3 years. It is not my primary duty, but as the JavaScript guy I’m the one who gets to read the documentation and figure out the best way to accomplish the tracking goals that other people would like to see. I am a frequent user of the front-end code, but I don’t spend all that much time actually working with the reports in SiteCatalyst. I spend far more time looking over Google Analytics reports because those reports are far more interesting to a web developer like me. I leave the SiteCatalyst reports for marketing guys to try to improve their brands.

    > I haven’t found performance issues to be extremely widespread during my four
    > years working with and supporting the tool, we’ve actually made a number of
    > platform improvements over the past year that improve performance
    > significantly.

    Okay, “woefully slow” is a bit of an overstatement, it is slow when compared to Google analytics but the Google product doesn’t have anywhere near as many fine grain selects. And Omniture is indeed faster than Piwik for absolutely any kind of report.

    > First, note that we’ve released a few “alternative” data collection methods
    > (PHP, Java, XML Data Insertion) over the past couple of years

    I haven’t even heard about the XML Data Insertion API before. That sounds absolutely awesome. If I were to build a new app that used Omniture reporting this would be the method I check out first. I could see that being a great way to skip the front-end development and dependencies. I’ll look into this further with one of the projects I’m working on. I’ll do another how-to if I end up recommending and implementing it.

    > SiteCatalyst admins can set up their own correlations (as well as managing
    > the majority of other settings) directly within the tool.

    As far as I know we still need to call Omniture client services to set up a custom collation report. I haven’t seen anywhere in the SiteCatalyst admin to build custom collations. But I could be wrong; I’ll dig around and see if I can find out for sure. Until then I’ve retracted the comment about having to call.

    Problems

    Indeed, most of the problems I’ve encountered with Omniture are the fault of the companies that use it, for some reason they never update. When I deal with the Omniture script I usually see H.5 through H.10 (circa 2006) because they never follow the updates. However, there have been so many problems that I’ve had to deal with it has left me a little jaded towards the script.

    For instance, recently I had a client with a site that had an inexplicable problem with a JavaScript form validation script. For some reason a validation event for one form was being run on every form on the page and always failing because those forms didn’t have the fields it was trying to validate. After a long diagnosis I found out it was because of the Omniture code on the page.

    This site had two Omniture scripts, version H.5 and H.17, the Omniture SiteCatalyst code (I believe the H.17) was storing existing form events in an array that uses the form name attribute as the key, specifically s_c_il[1].fa.os. Form name hasn’t been an acceptable identifier since the DHTML days so in modern XHTML markup it is always undefined or an empty string. This caused a problem where a submit event attached to one form was being run for every form on the page because Omniture was trying to pull the event from s_c_il[1].fa.os[''], which means that whatever happened to be to last form on the page with a submit event had that event run for every form on the page.

    I have only seen this happen once, but it took ages to diagnose. I assume it was due to a conflict, or someone screwed up the configuration, but I never found the real cause or solution to the problem. However it bugged me because this is simply the wrong way to handle multiple events. You should use the AddEvent technique I used here or create a new event and store the old event as a function inside the new event.

    Suggestions

    I’d love to offer my suggestions for improving your script. My big recommendations are:

    • Don’t obfuscate the script. This is really pointless, there is nothing worth hiding and it just makes debugging a nightmare. Offer the same releases as every other JavaScript product, a minified version for live deployment and a well-formatted version for development and debugging.
    • Use asynchronous script loading. The Omniture code is pretty large and requires a relatively long initialization time (which can be improved by not obfuscating). Why not use a short setup code with only basic information like account name and config options. Then load and execute the core code after the page has downloaded. This will speed up page loads and make client script easier to maintain. It has the added benefit of making clients think about the config items that they customize.
    • Offer better support for multiple installs. I know this is a dumb complaint because there should be no reason to have multiple Omnitures on one site, but I see it all the time. I see it on clients who syndicate content or service providers that offer client customizations. Right now it is not uncommon to see two Omniture installs conflicting with each other. There are numerous ways to accomplish this, namespace objects by account name, use non-overriding event attachment, use overload methods that support account name, etc.

    There are other items that I think can use some love that will greatly improve the quality of your service but those are the big ones. I’m sure there are internal issues that I am not aware of, like backwards compatibility with your web service and old versions of SiteCatalyst, but even those can be overcome with some good planning.

    Thanks for commenting on my article. I’ve updated it to be a little bit less negative to your service without removing my complaints. The purpose of this article wasn’t to berate Omniture, I just feel obligated to mention such things when I talk about services.

  3. Hi Steven,

    although I really like your Javascript coding style I don’t agree with alot of the points above.

    Let’s start at the beginning:
    >> It’s slow, obfuscated (really? why do this?), bloated, impossible to debug and not built using modern practices.
    I recently did some speed testing for my implemenations and my s_code (H20.3 + tons of custom code) runs withing ~83ms in FF3.5 at the page load. I think that’s not too bad.
    Starting with H20.3 the pagecode is not obfuscated anymore and so can the transfer can be gzipped pretty well while loading.
    I’m sure the script could be reduced by some bytes, but it does a great job for cross-browser compatibility. Since we have pretty fast internet connections these days, 1-2k extra are not too bad.
    Impossible to debug – hm…by default I agree. But if there are really some problems with it, there are some ways to debug. Firebug is your friend.

    >> This object has many different methods and properties that are all ingeniously designed to confuse and torment would-be implementers.
    I agree. BUT.. the functions coders should use are documented.

    >> 500ms link delay
    If you’re passing the ‘this’ pointer to a href, OMTR actually only adds the 500ms timeout if the link will be opened in the same window AND the timeout will be shortened if the image returns faster than that. (look into RDC to get fastest response times)

    >> There is nothing about this in the documentation
    There is a whitepaper available talking about link tracking

    >> Use asynchronous script loading.
    This has pros and cons. Some users want to send tracking as soon as possible even if the page has not fully loaded.

    >> Save this code in a js file and link that file on every page in our Omniture-enabled web application
    Ouch! Don’t do it… I like your idea of saving execution time by tweaking the code but including this script as an additional file would add so much additional load time (TCP connection, load time, etc.) that any time-savings by tweaking are lost.
    If you really want to load this code on any page, add it to your existing Omniture script.

    Ok, let’s get back to the initial purpose of your code.
    I’m not sure why you do not use existing Omniture code and so benefit from existing event handlers. I think in regards of saving execution time you should go that route.
    Look into the s.linkHandler plugin and put your code into the s_doPlugins function. Omniture already observes all clicks onto the document and so there is no need to add additional handlers. Additionally you would get around your “Links added after page load” gotcha. If the link is added properly to the document event bubbling should do its job.

    Best,
    Andreas

  4. Richard

    Hey Steven –

    Have you tried using Mixpanel? http://mixpanel.com

    They are like a more modern analytics provider that does basically what you want!

    Richard

  5. @Andreas

    Thanks for your input, it is really helpful to hear some other opinions and options.

    > I recently did some speed testing for my implemenations and my s_code (H20.3
    > + tons of custom code) runs withing ~83ms in FF3.5 at the page load. I think
    > that’s not too bad.

    83ms isn’t a terrible number, but it isn’t great either. That’s about the same number that my testing revealed.

    > Starting with H20.3 the pagecode is not obfuscated anymore

    Oh thank god. It’s about time. This is my single biggest complaint about Omniture. It’s just plain dumb to obfuscate a tracking script.

    > OMTR actually only adds the 500ms timeout if the link will be opened in the
    > same window AND the timeout will be shortened if the image returns faster

    I wasn’t aware of this. In my experience there is a noticeable lag on dynamic links (like jQuery tabs) in pages when they get tracking code attached to them.

    > look into RDC to get fastest response times

    Omniture Regional Data Collection is something that I am looking into for a couple products. This is another huge improvement for the service. But it is a bit of a pain to set up and get running. From what I understand it works just like a CDN. Why wasn’t 2o7.net simply converted to run this system by default?

    > including this script as an additional file would add so much additional
    > load time … add it to your existing Omniture script

    Ahh, yeah of course you are right on this one. I’ll fix that.

    > I’m not sure why you do not use existing Omniture code and so benefit from
    > existing event handlers. I think in regards of saving execution time you
    > should go that route.

    Can you be more specific? I don’t know of any way to hook into Omniture events to attach code like this.

    > Look into the s.linkHandler plugin

    I’ve had some bad experiences with Omniture plugins (for instance the form issue I cited earlier). Can you point me to some documentation for this plugin?

    @Richard

    Thanks for the link. I’ll have a look at them. The first thing I did was view source on their page, where I found this:

    // Yep, we use GA too, we do two very different things...for now. =)
    var gaJsHost...

    lol I like them already.

  6. Steven,
    since a single additional image/css/js/…-load is far above the ~83ms I don’t care about this insignificant time ;)
    If you see a lag for your tabs, just pass a “true” so the first param and you should be good.
    Why is RDC a pain to set up? Unless you’re using FPC this is a fairly easy change. Just get the approval from the AM and then change 1-2 lines of code. Can’t see any pain points here.
    Make sure you’re using the s_doPlugins function. It get’s called on all clicks (+ s.t(l) calls + media calls). You can use var u=s.p_gh(); to get the URL of the element that was clicked. (s.p_gh is a utility function you need to add, just google it). Use var o=s.eo?s.eo:s.lnk; to get the object that was clicked. The s.linkHandler can be used to do some automatic filtering, e.g. var u=s.linkHandler(‘/section/’) to only get links to a specific section. (there are also s.exitLinkHandler and s.downloadLinkHandler).

    Andreas

  7. Thanks for the update, Steven. I wasn’t commenting in an attempt to get you to remove any negative tone, per se, I just wanted to be clear about some changes, and about our attitude toward good developer feedback. (Incidentally, Andreas beat me to a few points, which is great. He probably knows the Omniture JavaScript code as well as just about anyone outside of Omniture.)

    I completely understand your point about form names. It looks like the site was using the “Form Analysis” plug-in, which is a nifty little code snippet that can, in cases such as these, be a complete nightmare. We actually don’t even give it out anymore unless the customer is working with our Omniture Consulting group so that we can help avoid situations like the one you described. In many, many cases, there are more efficient, more powerful, and just plain better ways to analyze form usage/abandon.

    As Andreas pointed out, we’ve made a number of code improvements over the past several years. H.5 code puts you, I believe, in late 2005; even H.17 is almost two years old now. Many of the incremental improvements we’ve made since then help to address some of the issues you mentioned; in any case, I will definitely be sharing your feedback (which was extremely detailed — you obviously know what kind of info developers need!) with our Engineering and Product Management teams.

    > Why wasn’t 2o7.net simply converted to run this
    > system by default?

    That is a great question, and I don’t have the answer presently. I’ll do my best to find out and report back to you. Generally, though, we’ve tried to make the RDC transition as painless as possible.

    > I’ve had some bad experiences with Omniture plugins
    > (for instance the form issue I cited earlier). Can
    > you point me to some documentation for this plugin?

    The linkHandler plug-in is fairly innocuous, but I am guessing it would actually be even easier for you to write your own “plug-in” to add an event handler to every link on the page. I did something similar for automatic exit link tracking a few months ago; you can read about it at http://is.gs/6n. (NOTE: WordPress keeps deciding to replace my straight quotes with smart quotes, even inside of a <pre> and a <code>, so if you decide to use the code make sure to locate and change those to straight quotes.)

    Anyway, this is a fantastic conversation and, as I said, I will be circulating it around the office. Feel free to let me know if you have any other concerns that I might have missed.

    Thanks,
    Ben

  8. Oh, I forgot to mention your point about multiple installs: I could not agree more. I would love to see this handled by allowing admin users to quickly and easily control the object name that their code is using. Using ‘s’ could still be default, but other options could be made available. While an adept developer can make this change manually, we still recommend letting our ClientCare team generate code when using an object name other than ‘s’. That process could certainly be improved. Action item for me.

    Thanks again,
    Ben

  9. @Andreas

    Thanks for the specifics. Very helpful. I’ll try to build a proof of concept with the info you gave me.

    > Why is RDC a pain to set up? … this is a fairly easy change.

    For normal situations, yeah it’s easy. But I would have to make that change for, well a lot of sites. My company has over 500 Omniture enabled sites live. Every little task becomes far more complex when you have that many sites. I will need to change how we do Omniture one day, but it will probably be a while before I can dedicate the time that it will take.

    @Ben

    I hope my suggestions will be helpful. Thanks for taking the time to review the article and my complaints about Omniture. I too am glad we got to have this conversation. I’ve learned quite a few things about Omniture from talking with you and Andreas.

    I’ve read your exit link article. The technique is very similar to mine but we have different coding styles. In fact the overall process is exactly the same, iterate through links, attach events, run events. We use some different keywords and structure, but overall they are very similar.

  10. Hey Steve,

    Thanks for the insightful post – but I’ve also good disagree with your implementation :)

    For many webpages, this script will be attaching hundreds (thousands?) of event handlers. That’s going to be slow.

    Plus if you dynamically add any links to the page after the fact, then you’re tracking code isn’t going to run on them.

    Much better to use an event delegation approach and attach a single event handler to the document element and then listen for clicks whose target is a link. This is how jQuery’s “live” method works.

    http://api.jquery.com/live/

    Karl Swedberg also wrote an excellent introduction to the technique on Learning jQuery:

    http://www.learningjquery.com/2008/03/working-with-events-part-1

    Cheers,

    John

  11. Hi John,

    I’ve played with the event capturing approach before, but I didn’t even think of using the event capturing technique when I built this. That’s a really good idea now that you mention it. Since I’m basing the search off of markup elements anyway I can rewrite this whole thing to use one event to capture every link. It would even fix the “Links added after page load” gotcha.

    Thanks for pointing that out! I can’t think of any downsides to using that technique for Omniture tracking. I know there were some event bubbling issues in older browsers but I don’t even bother supporting browsers that old anymore.

    Also, for the record I’ve used this script on pages with hundreds of links before and saw no visible delay, but I’ve never benchmarked it, and I’m always on fairly powerful computers.

  12. “Use asynchronous script loading. The Omniture code is pretty large and requires a relatively long initialization time (which can be improved by not obfuscating). Why not use a short setup code with only basic information like account name and config options. Then load and execute the core code after the page has downloaded. This will speed up page loads and make client script easier to maintain. It has the added benefit of making clients think about the config items that they customize.”

    – I need to do exactly this. We are now loading all of our js files asynchronously except for s_code.js, and Firebug is showing that even though it’s right before our tag, it is blocking most of our images. Just wondering if there are any pitfalls. Having trouble finding anyone who’s done this before.

  13. Steve, this post was fantasic. I only wish Omniture had included this in there own documentation. I couldn’t find it anywhere…. maybe they need some SEO… ;)

  14. Resurrecting a somewhat old thread to thank you for posting this—I came across your article after implementing a similar system myself, and it was good to have that approach validated by such a thorough article!
    The one addendum I will add is to link to my own version of that sb_tracklinks.track() function: I took a slightly different approach that takes a few more options, primarily to use an object-based approach to defining events, eVars and props but also to let you point events at an arbitrary tracking suite for debugging purposes. It’s a little heavier than your code, but for us it improved readability within our jQuery event handlers.
    I posted the code at https://gist.github.com/2470993

  15. Jesse Pakin

    I’ve thought about what you’ve got here. I did something very similar but i have a jquery dependency – the reason being i invoke the track and inits through pubsub (jquery events). My thinking was this would be the most unobtrusive implementation. If jquery isn’t available I have bigger fish to fry. :-)

    But what do you think about pubsub in general for this task? I know there are jQuery dependency free mechanisms, so if you really want to go dependency free you could try that route.

  16. Try This asynchronous Library it can help too!

  17. Audrey

    I know this is old, but I’m going to ask anyway:

    Any word on an updated version of the concept? You’re one of two people with useful posts on automatic link tracking. We currently use Omniture’s setupLinkTrack plugin, but it also has the “links added after page load” issue. Throw dynamic elements and AJAX into the world of Omniture, and you’ve got a mess.

    > …I can rewrite this whole thing to use one event to capture
    > every link. It would even fix the “Links added after page load” gotcha”

  18. Tom Rogers

    Steven,

    Line 4 of your script is pointless and may be deleted.

    The idea, of course, is that you don’t want to re-initialize your sb_trackLinks object if it already exists.

    However, line 6 explicitly re-declares the entire object, rather than overriding the init, track, and addEvent properties the extant object may already possess.

    I think the overall pattern is just fine, it’s just that line 6 guarantees that line 4 is irrelevant in all cases.

    That’s it! Thanks for the post. I agree that it can be unnecessarily difficult working with Omniture’s bizarre script API, and I appreciate the time you spent explaining some of the details of your implementation.

  19. Skara Brae

    Hi Steven,

    Thanks for the article, I came across it while implementing a similar thing. I was wondering if you had any thoughts or suggestions on the following.

    If you develop a Javascript link tracking library, and that captured data needs to be sent to a remote HTTP endpoint, how would you go about securing the remote endpoint against rubbish data being submitted?

    Given it’ll operate from the client’s browser, I was thinking some kind of one-time password whereby the server-side generates a key for each page request that the Javascript uses, and its verified on the remote server… but I haven’t put much thought into it.

    If anyone is aware of a servlet filter or some other open source mechanism which does this in a fairly simple fashion I’d be interested in the info.

Leave a reply

Pages linking to this article