Tracking link exits
Here at Hone, we have been strengthening our analytics and conversion metrics to give our customers the most insightful information about their audience. One useful event is that we track clicks to external sites. After searching for a good way to track our links, and not finding an elegant solution we decided to roll our own. We would like to share what we implemented.
⌗The issue
On the surface, it sounds super simple to just listen to all the click events on a given page and try to log them to a rest API. It’s a bit trickier than that. The issue is that all the XHR calls will be canceled once the page changes.
The first solution we found is to grab the event, and call event.preventDefault then set window.location after we have tracked the event.
window.addEventListener('click', function (e) {
var href = e.target.href
// this makes the xhr call
trackEvent('link exit', e, function (err, res) {
window.location = href
})
})
We were not a huge fan of this because it would take shift + clicks and ctrl + clicks and make them work the same way a normal click does when clearly that was not the user’s intent.
⌗Our solution
We decided that the event needed to stay an event and dispatch again once we have tracked it.
window.addEventListener('click', function (e) {
if (e.tracked) return
var mockEvent = new e.constructor(e.type, e)
mockEvent.tracked = true
trackEvent('link exit', e, function (err, res) {
e.target.dispatchEvent(mockEvent)
})
})
What is nice about this approach is that now the click events will work as intended. Whenever a user ctrl + clicks it still works because with this approach we copy the event via new e.constructor( e.type, e ), so all the modifier keys are still intact when dispatching the event again.
⌗Going further.
This is better but not a complete solution. There are a few things that would make this script a lot better.
⌗Link disabling
Since we are deferring the navigation of the link there is a higher chance the user will click the link again. We do not want two events just because of a slow connection. So we temporary disable the link and re-enable it once we have dispatched the event.
⌗Timeout
Sometimes an HTTP request can take more than 2 seconds. That is way too long for a user to wait after they have clicked a link. So instead of making the users wait, we will give the call a smaller amount of time to complete. It can be done by simply setting a short XHR timeout and then make sure xhr.ontimeout is handled. eg xhr.timeout = 1000
⌗Filtering link clicks
With the window click events we will get a lot more events that are not anchor tag clicks so we filter all those out. We also track internal navigation a bit differently than external linking so we filter out those events.
⌗Modifier handling
When a user is using a modifier key, lets say ctrl, tracking is not a problem because the page will not change. So instead of waiting we just dispatch those events right away.
Here is the full code:
window.addEventListener('click', function (e) {
// this means the click has already been canceled
if (e.defaultPrevented) return
// ensure link
var el = e.target,
origin,
mockEvent,
dispathed
// checking parents to see if we are nested in an anchor tag
while (el && 'A' !== el.nodeName) {
el = el.parentNode
}
// check the el is an achor tag
if (!el || 'A' !== el.nodeName) {
return
}
// getting origin
origin = location.protocol + '//' + location.hostname
if (location.port) {
origin += ':' + location.port // add port to origin
}
// same origin
if (el.href && 0 === el.href.indexOf(origin)) {
return
}
// check to see if we already tracked the event
if (e.tracked) {
return
}
mockEvent = new e.constructor(e.type, e)
mockEvent.tracked = true
if (e.metaKey || e.ctrlKey || e.shiftKey) {
dispatched = true // set this so we dont send this out twice
} else {
e.preventDefault()
}
trackEvent(
'link exit',
e,
function (err, res) {
if (!dispatched) {
e.target.dispatchEvent(mockEvent)
}
},
1500 /* this sets the timeout */
)
})