This weekend a user posted an interesting question to my forums. He wanted to know if there was a way (in ColdFusion of course) to determine how many seconds a user spent on a page. I decided to give this a try myself to see what I could come up with. Before we look at the code though, there are two things you should consider.
Number one - there is no ironclad way to actually determine the real amount of time a user spends looking at a web page. Yes, you can estimate it, but if I open your web page and than alt-tab over to my World of Warcraft session, then obviously the stats for my time on that page aren't accurate. So keep in mind that any numbers you get here will simply be estimates.
Number two - when it comes to web stats in general, I've found that it's almost always easier to let someone else worry about it - specifically Google. I remember parsing DeathClock.com logs and waiting 12+ hours for a report. The day I stopped parsing log files and just used Google Analytics was a good day indeed. GA does indeed provide this stat. (By the way, you guys spend, on average, one minute and forty-nine seconds on my site.)
So with that said, how can we track this in ColdFusion? There are many ways, but here is one simple method. I began by creating a session variable to store the data:
<cffunction name="onSessionStart" returnType="void" output="false">
<cfset session.pages = []>
</cffunction>
The pages array will store the information I'm tracking. I decided on the Session scope as opposed to the Application scope as I wanted to keep it simple and just provide a report for the current user.
Next, in every onRequestStart, I look at the array. For each page request I'm going to log the URL and the current time. I'll then look at your last page and store a duration:
<!--- Run before the request is processed --->
<cffunction name="onRequestStart" returnType="boolean" output="false">
<cfargument name="thePage" type="string" required="true">
<cfset var data = "">
<!--- determine if we have a last page. --->
<cfif arrayLen(session.pages)>
<!--- the last page's value is the timestamp, update it with the diff --->
<cfset session.pages[arrayLen(session.pages)].duration = dateDiff("s", session.pages[arrayLen(session.pages)].timestamp, now())>
</cfif>
<cfset data = {page=getCurrentURL(),timestamp=now()}>
<cfset arrayAppend(session.pages,data)>
<cfreturn true>
</cffunction>
Nothing too complex here really. If my session.pages array has any data, I must be on the second (or higher) page request. I do a quick dateDiff and store the result in the duration field. Outside the cfif I do a quick array append of a structure containing the current page url and time. The function getCurrentURL comes from CFLib.
The last thing I do with the data is to serialize it and store it when the session end:
<!--- Runs when session ends --->
<cffunction name="onSessionEnd" returnType="void" output="false">
<cfargument name="sessionScope" type="struct" required="true">
<cfargument name="appScope" type="struct" required="false">
<cfset var data = "">
<cfset var filename = "">
<!--- serialize --->
<cfwddx action="cfml2wddx" input="#arguments.sessionScope.pages#" output="data">
<!--- save it based on the sessionid value --->
<cfset filename = expandPath("./" & replace(createUUID(),"-","_","all") & ".txt")>
<cfset fileWrite(filename, data)>
</cffunction>
That's it. There are a few things I'd probably do differently if I were to really deploy this code. First I'd use the database to store the updates. With a nice stored procedure it should run rather quickly. Even if I didn't do a DB call on each page update I'd at least change onSessionEnd. Notice that that you will have a 'hanging' page at the end with no duration. You could simply delete that from the array. Or you could use that last page and store it as another stat - the exit page.
I whipped up a real simple index page that dumps the session data and lists a few quick stats:
<cfdump var="#session#">
<cfset times = []>
<cfloop index="p" array="#session.pages#">
<cfif structKeyExists(p, "duration")>
<cfset arrayAppend(times, p.duration)>
</cfif>
</cfloop>
<cfoutput>
<cfif arrayLen(times)>
Average duration: #arrayAvg(times)# seconds.<br/>
</cfif>
Total number of pages visited: #arrayLen(session.pages)#
</cfoutput>
And here is some sample output:

I've attached the code to the blog entry. Not sure how useful this is, but it's not like I've cared about usefulness in my other posts! ;)
Archived Comments
That's a good server only solution. Another way would be to go with javascript/ajax. There would be an Onload event which pings a cf page to register the start time for that page, then an Unload event to stop the timer for that page. This way, you always capture that last page.
I use this in my Turing test.
If someone has submitted a comment in less than a few seconds, I send them an error message instead of processing it.
One of my side projects require students to spend time on our website studying. As with Jules I use Javascript & Ajax to determine time spent in study mode. As for the Alt-tab to World of Warcraft session scenario, with both CF only or Javascript\Ajax solutions you can use the good old body onBlur & onFocus attributes (e.g. <body onblur="pauseTimer()" onFocus="startTimer()") to pause & restart your page timers.
Yaron beat me to the onblur/onfocus suggestion.
We use Gomez performance monitoring services, including among other things their "Actual XF" (no clue what the hell XF means, they just renamed a bunch of their products to use it recently) product, which sticks a bit of javascript on the page and tracks "Load Time, Perceived Render Time, DOM Ready Time, Response Time, Page Views, Satisfaction Index, Visitor Satisfaction, Visitor Satisfaction and Conversion, Custom Event Timings, Abandonment, Object Errors, and Cache Level" (swiping that from my "create a chart" page) as well as the usual stuff like browser, OS, color depth, ISP, etc.