Capturing a Visitor?s Clickstream

A nice feature that I?ve seen implemented on a few Web sites is the ability for the visitor to view their clickstream (essentially, the path they?ve taken through your site). This allows the user to jump back to a particular page they?ve visited at any time.

For a (very ugly) sample, see http://charlie.griefer.com/code/clickstream/

By providing this list to your users, you allow them to go, ?hey...where was I a couple of pages back when I saw that one picture...??...and then peruse the list of recently viewed pages.

I?ve chosen to build this as a custom tag. For those unfamiliar with custom tags, it?s simply a ColdFusion template that acts as a <cfinclude>, but can accept parameters. The reason for doing it as a custom tag is because it?s essentially the same code repeated over and over on each page. While this would ordinarily make it a candidate to simply be <cfinclude>?d, there is one difference from page to page...each page must identify itself in order to be added to the clickstream list. I will be passing that page name to the custom tag as an attribute.

The custom tag is called breadcrumb.cfm. A custom tag is called by adding ?cf_? to the name of the template, and dropping the extension.

So to include the custom tag in a particular template, the code would be:

<cf_breadcrumb>

The way I?ve put this together, breadcrumb.cfm accepts two attributes. One is the title of the current page (to be added to the list of visited pages). The other is the number of pages to ?remember? (by default, it will display the last 10 pages visited). So to call the tag with the attributes, the code would look like:

<cf_breadcrumb 
      pageTitle=
?Home Page? 
      trail_length=
?10?>

This code would be on the home page, of course. To call the tag from the ?About Us? page, it would be:

<cf_breadcrumb 
      pageTitle=
?About Us? 
      trail_length=
?10?>

...and so on.

Because the clickstream persists from page to page, and is unique for each user, it will be stored in a session variable. The name of the session variable is session.clickstream (original, yes?). 

Because we?re using session variables, there needs to be an Application.cfm. This Application.cfm is very simple. It creates a name for the application, using the <cfapplication> tag...which also specifies the timeout value of the session variables. It then checks to see if the session variable exists yet. If not, it creates it.

Here?s the complete code for Application.cfm:

<cfapplication 
        name=
"clickstream" 
        sessionmanagement=
"yes" 
        sessiontimeout=
"#createTimeSpan(0,0,20,0)#">

<cfscript>
    if (NOT structKeyExists(session, 'clickstream')) {
        session.clickstream = arrayNew(1);
    }
</cfscript>


The <cfapplication> tag is self-explanatory. The <cfscript> block is simply checking for the existence of the variable ?clickstream? within the session scope. It might look a bit unfamiliar, but it?s essentially the same thing as <cfparam name=?session.clickstream? default=?#arrayNew(1)#?>.

As you can see, session.clickstream is an array. This makes sense, as we?re going to be storing multiple values in one location.

Now to dissect the custom tag itself. The code for breadcrumb.cfm:

1. <cfparam name="attributes.trail_length" default="10">
2. <cfif NOT structKeyExists(attributes, 'pageTitle')>
3.    <cfexit>
4. </cfif>

5. <cflock name="addNewPage" type="exclusive" timeout="10">
6. <cfscript>
7.    if ((arrayIsEmpty(session.clickstream)) OR (compare(attributes.pageTitle, session.clickstream[arrayLen(session.clickstream)].title))) {
8.    if (arrayLen(session.clickstream) EQ attributes.trail_length) {
9.    temp = arrayDeleteAt(session.clickstream, 1);
10. }
11. temp = arrayAppend(session.clickstream, structNew());

12. session.clickstream[arrayLen(session.clickstream)].title = attributes.pageTitle;
13. session.clickstream[arrayLen(session.clickstream)].path = listLast(cgi.script_name, '/');
14. }
15. </cfscript>
16. </cflock>

17. <br /><br /><br /><br />

18. <cfoutput>Your Click Path:</cfoutput>
19. <br />
20. <table style="border:solid #000000 1px; width:150px;" cellpadding="1" cellspacing="0">
21.     <cfoutput>
22.     <cfloop from="#arrayLen(session.clickstream)#" to=?1" index="i" step="-1">
23.     <tr>
24.         <td><a href="#session.clickstream[i].path#">#session.clickstream[i].title#</a></td>
25.     </tr>
26.     </cfloop>
27.     </cfoutput>
28. </table>


Since breadcrumb.cfm is a bit involved, let?s go over it line by line.

  1. Breadcrumb.cfm is expecting two attributes to be passed to it. One is trail_length. If this attribute is not passed, we set it to a value of 10 by default.
  2. The other attribute that the tag expects is ?pageTitle?. This is important, as it is the name that will be appended to the clickstream. If it is not passed, we really don?t know what value to assign it by default. For that reason, we?re not even going to try. If it?s not provided, we use <cfexit> to gracefully exit the custom tag.
  3. The cfexit
  4. </cfif>
  5. Because we?re dealing with session variables, it is considered a ?best practice? to use <cflock>. This helps to ensure the integrity of the shared scope variables. 
  6. Open a <cfscript> block
  7. Line 7 can get a bit tricky. On my first draft of this code, I could hit ?Home Page? 10 times in a row, and my clickstream would be ?Home Page? repeated 10 times. Likewise, if I was on a page and I hit refresh, that page was added twice in succession to the clickstream. I didn?t want this behavior. No one page should show up twice (or more) in succession. So I realized I needed to check the title of the last element in the array with the current title. If they are the same, I don?t want to do anything. The compare() function returns 0 if the two arguments are equal. 

    The one problem with that is that if the array was empty (if the current page was the first page in my clickstream), the application errors out on the compare() function (because there *is* no last element to compare (there can?t be a last element if there are no elements). So the one condition under which I don?t want the compare() function to run is if the array is currently empty. Hence the if arrayIsEmpty(session.clickstream) which is added to condition.
  8. Line 8 checks the length of the array to see if it has met the maximum length as specified by the attributes.trail_length value. There?s no use in continuing to store values if the user is only going to see the first 10 (or whatever value is specified). So in order to avoid exceeding the value of attributes.trail_length, we remove the first element from the array once we hit the specified length.
  9. The actual deletion of the first element in the array (remember, CF arrays are dynamic in nature, and when the element at position 1 is deleted, the elements at positions 2 and higher will shift down one position to fill the void).
  10. Closing curly brace for the condition in #8.
  11. Now we?re ready to append the current template into the clickstream. Now you?ll see that each array position in session.clickstream is actually a structure (because we want to store both the name of the page and the URL). You?ll understand that a bit more on lines 12 and 13. For now, suffice it to say that we need to create a new array position, which will be a structure :)
  12. One of the keys of the structure is ?title?. This is referenced as session.clickstream[n].title (where n is a position in the array between 1 and the array length). The value of the key is the value passed in attributes.pageTitle. This is the value that is actually displayed to the user in the clickstream.
  13. The other key in the structure is ?path?. This value was not passed to the custom tag, but can be ascertained using the cgi environment variable #cgi.script_name# and the listLast() function. This value will allow us to make the individual items in the clickstream into links.
  14. ?
  15. ?
  16. ?
  17. ?
  18. Now we?re ready to display the clickstream to the user.
  19. ?
  20. Start an HTML table...
  21. Open a <cfoutput> tag...
  22. Now we?re going to loop over the session.clickstream array. So we loop from the length of the array to 1, with a step of ?1 (we want to loop backwards, so the most recently viewed page is at the top of the clickstream list).
  23. For each iteration of the loop, we want new table row (<tr>)
  24. Inside of a <td>, output the value of session.clickstream[i].title, inside of <a href> tags that contain the value of session.clickstream[i].path. The user now clearly sees the clickstream, with a link to any page contained therein.

    The rest of the code simply closes <tr>s and <table>s.

To implement, simply place the call to the custom tag on any pages that you would like included in the clickstream.  As shown earlier, it would look like this:

<cf_breadcrumb
         pageTitle=
"Contact Us"
         trail_length=
"10">

This would, of course, go on a Contact Us page (contactus.cfm(?)), and specifies that the clickstream should 'remember' 10 pages back.

While you'd probably want this code on most of the pages in your site, one caveat would be do NOT place the tag on form action pages.  A form's action page should only be accessible via the form itself.  If you do include the action page it would most likely simply display a "Warning Page Has Expired" message, which is of no use to your visitors.

That?s all there is to it. As always, questions, comments, criticisms, and large inheritances are welcome. Feel free to e-mail me, or post a question on the Easycfm.com forums.

About This Tutorial
Author: Charlie Griefer (CJ)
Skill Level: Intermediate 
 
 
 
Platforms Tested: CF5,CFMX
Total Views: 41,548
Submission Date: June 16, 2003
Last Update Date: June 05, 2009
All Tutorials By This Autor: 15
Discuss This Tutorial
  • I liked this but I wanted the display to be horizontal instead of vertical so here is what I did. I also implemented the BroChild fix posted above and this now works with subdirectory/links, too! Thanks BroChild! Oh, and I also added the option to clear the clickstream. We had clients request that they can clear their clickstream and this gave them a piece of mind. We informed them we weren't tracking their clicks but I digress, silly ole bear. Here is my breadcrumb.cfm file [code] ArrayClear(session.clickstream); if ((arrayIsEmpty(session.clickstream)) OR (compare(attributes.pageTitle, session.clickstream[arrayLen(session.clickstream)].title))) { if (arrayLen(session.clickstream) EQ attributes.trail_length) { temp = arrayDeleteAt(session.clickstream, 1); } temp = arrayAppend(session.clickstream, structNew()); session.clickstream[arrayLen(session.clickstream)].title = attributes.pageTitle; session.clickstream[arrayLen(session.clickstream)].path = listLast(cgi.script_name); }



    Clear Clickstream
    Your Click Path:
    #session.clickstream[i].title# >

    [/code]

  • Would it be possible to store this click stream in a database by storing all the info initially in a session variable and then writing to the database when the user logs out. Is there any way you could force the session variable contents to be written to the database when the user simply closes the browser. If you include the following code would the session information be preserved ? function changeurl() { window.open("write_session_to_database.cfm"); }

  • Never mind I fixed it by placing the customtag under the custom tag directory and removing the '/' from the list from the listlast function

  • Charlie: Great tutorial Its working great for me as long as all the files are under the same directory. As I am new to CFM, I tried all night (until 4:00 AM :) trying to set the path to match my root directory and its not working. I saw that you are setting the path from the array on line 13 of the tutorial. I would like to change it because I have lots of directories and would like the path to always match the correct directory. Can you show me how to do that ;) I appreciate the help

  • ..you could it, and simply pass the page name from a variable set in the page you are including it from, which the included page will read!

Advertisement

Sponsored By...
Powered By...