Outbound Link Hit Tracking With ASP.NET

By Peter Bromberg

Shows a simplified method for setting up a javascript include file that will wire up all your ASP.NET pages (or any page) to automatically log outbound link hit traffic to your database, using an ASHX image hander.

Outbound link tracking is becoming popular from an SEO (Search Engine Optimization) standpoint. You can see this because a variety of statistics providers are now offering it.  MyBlogLogAddFreeStats, RiteCounter, BlogFlux and Google Analytics (with a special scriptlet) are just a few.

Why would you want to track outbound links? The exit link can tell you where your visitors go after leaving your website or blog, and it tells you which links on your site are most likely to take your visitors away. You could describe it as "outbound traffic". But it also tells you what on your site or blog is most interesting to the visitor, and that can be very valuable in building content and choosing keywords.

I decided to build a simple outbound link tracker script to experiment with, and so the first step was to search around for some javascript that could automatically enable every page to capture outbound link clicks. I found several, but the one I found most easy to customize is called "Clicky".  Unfortunately, Clicky needed to be upgraded to support the Firefox browser (which I did) and it also needed to be souped up to provide automatic database inserts. The result is a nice script that can be included in your MasterPages which points a dynamicaly generated <img.. tag at an ASHX handler I whipped up to log the outbound clicks. In my particular scenario, I am more interested in counting "most popular" outbound clicks than in logging every single click, so my insert stored proc does an "insert or update" and increments the count column. This makes it very easy to have a report page that will give me a list of outbound clicks that can be ranked by popularity.

First, let's take a look at my customized Clicky.js include client script, and then I will show the ASHX handler codebehind:

var clicky_link_url_arg = "&link=";
var clicky_text_url_arg = "text=";
var clicky_stamp_url_arg = "&stamp=";
var clicky_image_load_pause_msecs = 500;
function clicky_getDomain(url) {
    var i = url.indexOf("://");
    if (i == -1) {
        return "";
    }
    
    url = url.substring(i + 3, url.length);
    i = url.indexOf("/");
    if (i != -1) {
        url = url.substring(0, i);
    }
    return url;
}

function clicky_handleError() {    
    return true;
}

function clicky_report_onclick(ev) {    
    var oldonerror = window.onerror;
    window.onerror = clicky_handleError;
    clicky_report_onclick_inner(ev);    
    window.onerror = oldonerror;
    if (old_onclick) {        
        old_onclick(e);
    }
}

function clicky_report_onclick_inner(ev) {
    var text;
    var url;
    var target;
    if(ev) {
        target = ev.target;
    }
    else {
        target = window.event.srcElement;
    }

    for (var i=0; target && i<=20; i++) {        
        if(target.href) {
            break;
        }
        target = target.parentNode;
    }

    if (target && target.href) {
        url = target.href;        
        if (target.innerHTML) {
            text = target.innerHTML;
        }
        else if (target.innerText) {
            text = target.innerText;
        }
        else if(target.text) {
            text = target.text;
        }
    }

    if (!url || typeof(url) != 'string' || url == "") {
        return;
    }
    if (!text || typeof(text) != 'string' || text == "") {
        text = "[no text found]";
    }
    
    var currentDomain = clicky_getDomain("" + window.location);
    var currentBaseDomain = currentDomain;
    
    if (currentBaseDomain.indexOf("www.") == 0) {        
        currentBaseDomain = currentBaseDomain.substring(3, currentBaseDomain.length);
    }
        var expressionStr = "(\\w+[.])*(" + currentBaseDomain + ")$";
    var regex = new RegExp(expressionStr, "i");
    if (text.length > 200) {
        text = text.substring(0, 200) + "...";
    } 
    if (!clicky_getDomain(url).match(regex)) {        
        var timeStamp = new Date();
        var reportUrl = "http://" + currentDomain + "/ClickLog/Pix.ashx?" + clicky_text_url_arg + escape(text) + clicky_link_url_arg + escape(url) + clicky_stamp_url_arg + timeStamp.valueOf();
        //alert(reportUrl);
        var image = new Image();
        image.src = reportUrl;        
        var now = new Date();
        var stopTime = now.getTime() + clicky_image_load_pause_msecs;
        while(now.getTime() < stopTime) {
            now = new Date();
        }
    }
}
 try{
 window.captureEvents(Event.CLICK);
   window.onclick = clicky_report_onclick
 }
   catch(e) {}
 var old_onclick = document.body.onclick;
document.body.onclick = clicky_report_onclick;
Sorry about the try /catch at the bottom, I just got real frustrated trying to wire up a document onclick handler for FireFox and took a shortcut! You want to include the above script at the very bottom of your page, and I'd recommend using the defer attribute to ensure that this script isn't processed until everything else in the page is loaded. So, a simplified script tag would look like so: ...
<SCRIPT src="clicky.js" defer="defer"></SCRIPT>
If you take a look at the script, you can see that it constructs a dynamic Image object that points to my ASHX Handler:
using System;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

namespace ClickLog
{
    public class Pix : IHttpHandler
    {
        public bool IsReusable
        { get { return true; } }
        public void ProcessRequest(HttpContext ctx) 
   { 
   string ip= String.Empty;
   string referer=String.Empty;
   string link = String.Empty;
   string text = String.Empty;
           if(ctx.Request.ServerVariables["REMOTE_ADDR"]!=null)
             ip=ctx.Request.ServerVariables["REMOTE_ADDR"];
             if(ctx.Request.ServerVariables["HTTP_REFERER"]!=null)
             referer=ctx.Request.ServerVariables["HTTP_REFERER"]; 
             if(ctx.Request.QueryString["link"]!=null)
            link=ctx.Request.QueryString["link"];
            if(ctx.Request.QueryString["text"]!=null)
            text =ctx.Request.QueryString["text"];
             
/*
dbo.InsertClickLog
@Link varchar(1000),
@Text varchar(200),
@Referer varchar(1000),
@RemoteIp varchar(100)
*/

        string connectionString = ConfigurationManager.ConnectionStrings["clicklog"].ConnectionString;
            SqlConnection cnn = new SqlConnection(connectionString);
            SqlCommand oCmd = new SqlCommand("dbo.InsertClickLog",cnn);
            oCmd.Connection = cnn;
            try
            {
                cnn.Open();
                oCmd.CommandType=CommandType.StoredProcedure;
             
                oCmd.Parameters.Add(new SqlParameter("@Link",SqlDbType.VarChar,1000));
                oCmd.Parameters.Add(new SqlParameter("@Text",SqlDbType.VarChar,200));
                oCmd.Parameters.Add(new SqlParameter("@Referer",SqlDbType.VarChar,1000));
                oCmd.Parameters.Add(new SqlParameter("@RemoteIp",SqlDbType.VarChar,100));
                 
                oCmd.Parameters["@Link"].Value = link;
                oCmd.Parameters["@Text"].Value = text;
                oCmd.Parameters["@Referer"].Value = referer;
                oCmd.Parameters["@RemoteIp"].Value = ip ;
                 
                oCmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ToString());
            }
            finally
            {
                cnn.Close();
                oCmd.Dispose();
            }
   }
    }
}

That is pretty much all you need! In the downloadable solution below, I've included a SQL script that will create the ClickLog table and the dbo.InsertClickLog stored proc you need. I've also included a version of my "db.aspx" script-only database viewing page that allows you to dynamically view your results.


                                                 Sample display of results

There is a test page with a link to "yahoo" as well as a link to the report page. To set this up, all you need to do is unzip, create a virtual directory, run the SQL Script against your favorite test database, and modify the connection string in the web.config to match your environment.  Comments, suggestions, or improvements are always welcome!

Download the Visual Studio 2005 solution that accompanies this article.

Popularity  (1491 Views)
Picture
Biography - Peter Bromberg
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites. Follow Microsoft MVP
Create New Account
Article Discussion: Outbound Link Hit Tracking With ASP.NET
Peter Bromberg posted at Sunday, February 04, 2007 7:07 PM
Firefox 2.0 double count issue
goran grgic replied to Peter Bromberg at Sunday, February 04, 2007 7:33 PM

Hello Peter,

 

One question;

 

I don’t know if you experienced this behavior with your Outbound Link Hit Tracking With ASP.NET.

When I click on links in Firefox 2.0 I get double count for each click link.

But when tested in IE 6.0 for example, it count’s them ok.

 

I tried it with your ‘testpage.htm’

 

Do you know what could be the cause of it ?

 

Thanks,

Goran Grgic.

goran grgic replied to Peter Bromberg at Sunday, February 04, 2007 7:33 PM
Ok ..
I think I figured it out fixes the issue with FF2
I tested old_onclick event and it works fine with FF1.5, FF2, IE6 and IE7

Doing coment on js block:
// try{
// window.captureEvents(Event.CLICK);
//  window.onclick = clicky_report_onclick
// }
//catch(e) {}