Using Ajax to Count Clicks

LINK'd

For a bunch of years I’ve had my own database-driven links page I call LINK’d. I built it way before the terms “social bookmarking” or “tags” entered the vernacular, and while sometimes I think about exporting the database and moving the whole thing over to del.icio.us or ma.gnolia.com, I’m kind of attached to my site and it sometimes gets me thinking about things I wouldn’t normally think about.

This article is a result of one of those random thoughts.

Google Hates LINK’d

I was digging through LINK’d looking for something when my eye wandered into my browser’s Google Toolbar and I realized that LINK’d wasn’t getting much love from Google. Its page rank was a whopping zero, and the AdSense ads on the site never really tied into the content, which led me to believe something wasn’t working quite right. It got me thinking about what was screwy about the page.

I’d already given the page a CSS-makeover a couple of years ago and it makes use of semantically-correct markup using header and paragraph tags to display the links in different categories, so that probably wasn’t it; search engines should love the page.

It finally dawned on me — maybe my links stink.

Lousy Links

Back when I first created LINK’d, I thought it would be kind of neat to see what people were clicking on. I wrote some link tracking code that worked using a redirect, so the links didn’t actually go to the site being linked to. Instead of linking straight to a page, all the links looked something like this:

http://www.lmnopc.com/links/index.php?action=jump&url=http%3A%2F%2Fwww.google.com&id=1

The action=jump told my web app that the code was to fire the redirect script. url specified what site to redirect the user to, and id specified what link counter record to update in the database.

Google and other search engines aren’t going to like that very much because to a spider, all my links point right back at the page that sourced them. Not good.

Fixing My Links

The first thing I did was nuked the nutty jump URLs and replaced them in the code with links straight to the sites they really were supposed to point. Next, I went in and added some javascript to pass the link_id used to reference that link in the database to a function like this:

<a onclick="countClick(1); return true;" href="http://www.google.com/" />

Adding Ajax

The next step is pretty obvious — write a function named countClick(). countClick is where Ajax (more accurately xmlHttpRequest) comes into play. What countClick is going to do is make an asynchronous request back to my index.php file with a URL containing a query string telling the script that a link’s been clicked and the PHP will run a query that will UPDATE the counter of the specified link.

countClick is really simple

function countClick(id)
{
   loadXMLDoc('/links/index.php?click=' + id, ajaxCallback);
}

loadXMLDoc is based off some code I found over at Apple’s Developer Connection. You can find that code here: http://developer.apple.com/internet/webcontent/xmlhttpreq.html. My version is a tiny bit different. I changed it so that instead of always falling back to the same request handler, my function accepts a function as a parameter that it uses for the xmlHttpRequest callback. My changes are highlighted:

var req = false;
function loadXMLDoc(url, funcProcess) {
   req = false;
   if(window.XMLHttpRequest) {
      try {
         req = new XMLHttpRequest();
      } catch(e) {
         req = false;
      }
   } else if(window.ActiveXObject) {
      try {
         req = new ActiveXObject("Msxml2.XMLHTTP");
      } catch(e) {
         try {
            req = new ActiveXObject("Microsoft.XMLHTTP");
         } catch(e) {
            req = false;
         }
      }
   }
   if(req) {
      req.onreadystatechange = funcProcess;
      req.open("GET", url, true);
      req.send(null);
   }
}

I bolded my awesome (haha) changes.

Here’s my callback function:

function ajaxCallback()
 {
   if (req.readyState == 4)
   {
      if (req.status == 200)
      {
         if (req.responseText.length)
         {
            if (req.responseText != '1')
            {
               alert('countClick Failed');
            }
         }
      }
      else
      {
         alert("There was a problem retrieving the XML data:\n" + req.statusText);
      }
   }
}

99% of that code is standard request handling stuff. I highlighted the one line that’s not standard. All it does is show an alert to say something didn’t happen right. We’re not trying to make a web-based word processor or email application here, so I’m not going overboard here. If I wanted some kind of fault handling, I’d use one of the dozen or so Ajax frameworks out there for PHP instead of going the quick and dirty route that I’m taking here.

The Server Side

Now that we have the client side of this app done, we have to write a little PHP to handle requests made by the Javascript to update the counter.

Before talking about the code, I should probably talk a little bit about the schema behind this application. Here are the fields I’m focusing on:

link_id               Each link has a unique id stored as an integer
url                   The target site's URL
views                 The number of times the link has been clicked
last_viewer           The ip address of the last person to click on this link
last_viewer_timedate  The time/date that the counter was last incremented

I’m not going to go into a ton of detail on the PHP side of things. I think the comments do a pretty good job describing what’s going on in this snippet:

// look for querystring that tells program to increment
// the click counter on a given link_id
if (isset($_GET['click']))
{
	// retrieve the incoming link ida
	$link_id = $_GET['click'];

	// make sure id is numeric
	if (is_numeric($link_id) == false)
	{
		exit('0');
	}

	// retrieve the user's ip address
	// this is used to prevent someone from
	// clicking on a counter a zillion times
	// to artificially inflate a counter
	$ip = wrap_quotes(getenv('REMOTE_ADDR'));

	// assemble the SQL statement incrementing our counter
	$sql = "UPDATE links_links
	      SET views=views+1, last_viewer=$ip, last_viewer_timedate=NOW()
	      WHERE link_id=$link_id AND last_viewer!=$ip;";

	// execute the SQL statement
	mysql_query($sql);

	// if we get to this point, we can assume that everything
	// went as planned, so we print the '1' the Ajax script is
	// expecting to signal success
	exit("1");
}

Good luck!

I hope you find this article helpful, and if you run into any problems, feel free to write and I’ll try to answer any questions you may have.

15 Responses to “Using Ajax to Count Clicks”

  1. Hi. I tried your solution (which would be brilliant) but wasn’t able to make it work. Lots of errors from the req.status which is apparently a Firefox bug, but I wasn’t able to make it work no matter. what. Great article, it made me think a lot and find another (less elegant) solution.

  2. Jeff – Glad you liked the article and sorry you ran into an issue. A quick way to get this sample working is to just add a global variable to the script.

    Right above loadXMLDoc, add a line to declare req:

    var req = false;

  3. Thank´s a lot for this code. Nice solution. I was looking a lot of click tracking tools and all of them were disgusting. I found http://www.labsmedia.com/clickheat/ interesting too, but it didn´t ruled for pages with frames (were i try to implement the solution).

  4. Hi Thom, first of all thank you for your code, I tried to implement it in a test page im developing and i couldn’t make it work. I debbuged it in firefox using firebird and notice that it catching an error in the line

    req = new XMLHttpRequest();

    Because the req variable was not assigned correctly, it had a readyState value of 1 in the ajaxCallback and got the alert error. I would appreciate if you could help me with this issue.

    Thank you very much

  5. Marco – You’re running into the same thing as Jeff. req needs to be declared outside of loadXMLDoc so that it’s accessible to the callback.

    I’ve updated the code so others won’t run into this in the future. :)

  6. Thank you very much!

    This is exactly what I was looking for!

    The only thing I don’t get, is why this would improve your pagerank and the Adsense-ads.

    Anyway, many thanks,
    Joshua (I’m Dutch, so don’t blame me for incorrect spelling)

  7. Hello, nice solution. I will use it in the future. I have one question. How do you know which visitor is a search engine, cause I don’t want to count clicks of search engines?

  8. @ Joshua

    Read the section titled “Lousy Links”. To search engines, all the links point to http://www.lmnopc.com — not the actual site that the text describes, so it looks like none of the links lead to good content. Using this method, the links actually point to the content they describe.

    @ Beeuser

    Search engine crawlers don’t use Javascript, so they won’t be triggering the script.

  9. try your code, it not working, error object expected, where can i download to test.

  10. When the 3rd parameter sets the request to be run asynchornously
    req.open(”GET”, url, true);
    I ve encountered difficulties when the page was reloaded before the request was done, so this click was “ignored”. Changing it to false should solve the issue.

    Also, maybe it’s useful to point out that if user makes a double (or quadruple or whatever ) click on the link, the request will be executed twice (or four times and so on).

  11. It is also very important to disable caching for the called url, otherwise the browser can take the request response from local memory without really executing the request.

    as found on php.net we can use:
    header(“Cache-Control: no-cache, must-revalidate”); // HTTP/1.1
    header(“Expires: Mon, 26 Jul 1997 05:00:00 GMT”); // Date in the past

  12. Pablo : Thanks for your comments.

    I’ll have to update my stuff to take your notes into account.

    One thing that I’m doing is remembering the last IP address that clicks each link, so double-clicks (quadruple, octa-click or whatever) are ignored.

  13. great and huge stuff i like it thanks

  14. I must thank you for this post because it seems to do exactly what i was looking for. But, as a total newbie in ajax, php and all this stuff, i was wondering if you could write a more detailed step by step guide to implement your script on a wordpress blog…
    I’m really looking forward to it.
    Thanks.

    Steker

  15. Many thanks offering this, That it is exactly what I was looking in bing. I’d prefer to pick up opinions through somebody, instead of a business website, that’s exactly the reason why I love weblogs so much. Thanks!