Ask a Jedi: Tracking Users

Paul asked me this morning if there was a simple way to track the users currently using your site. There are a couple of ways you can handle this. Let's consider a simple example where you want to track known (i.e. logged in) users of an application. This means two things:

  1. When a user logs on, add her to a list of logged in users.
  2. When a user logs out, remove her from the list.
In general this is trivial to do, but before ColdFusion 7, it was difficult to handle cases where the user did log out, but simply had their session time out. (As a quick aside, there are 'ServiceFactory' methods that let you inspect the current sessions of an application, but this blog will focus on supported methods only.)

First off, let's build a method to store the logged in users. An application variable makes sense for this, so let's default it in the onApplicationStart method of our Application.cfc file:

<cffunction name="onApplicationStart" returnType="boolean" output="false"> <cfset application.users = structNew()> <cfreturn true> </cffunction>

In the onApplicationStart method above, I created a structure that will store my users. A list or array would work fine as well, but a struct lets me store more information. I'll show an example of that later.

To record the user, all you do is update the structure when the user logs on. This code will be unique per application, but assuming "username" equals their username, you could store them like so:

<cfset application.users[username] = structNew()>

I stored the user as a blank structure, but you could store information in there like their real name, age, or whatever. You could also update information as the user browses the site. Consider:

<cffunction name="onRequestStart" returnType="boolean" output="false"> <cfargument name="thePage" type="string" required="true"> <cfif userLoggedOn> <cfset application.users[username].lasthit = now()> </cfif> <cfreturn true> </cffunction>

In the onRequestStart code above, I checked to see if the user is logged in, and if so, I record when the user last hit the site. (Obviously the variable names and conditionals would change based on your application.) By storing the last hit, I could do interesting things like seeing how active the users are on the site.

If the user logs out, you will need to remove them from the application variable. In a logout() method, you would do:

<cfset structDelete(application.users, username)>

And as I mentioned above, you need something similar to handle the session timing out:

<cffunction name="onSessionEnd" returnType="void" output="false"> <cfargument name="sessionScope" type="struct" required="true"> <cfargument name="appScope" type="struct" required="false"> <cfif structKeyExists(arguments.appScope, arguments.sessionScope)> <cfset structDelete(arguments.appScope, arguments.sessionScope.username)> </cfif> </cffunction>

The only difference here is that both the session and application scopes are passed as arguments. You can't reference them directly.

Lastly, to answer the simple question of getting a count of users, a simple structCount(application.users) would return the number of users. If you store other information, you could return the number of boys versus girls, lefties versus right handers, or whatever else you may know about users.

Archived Comments

Comment 1 by Paul Jones posted on 8/21/2006 at 5:37 PM

Thanks Ray - this should help me out no end. You're a gentleman and a coder sir!

Comment 2 by Roland Collins posted on 8/22/2006 at 2:56 AM

I would use a named lock on at least the add and delete operations. The update operations should be fine to leave unlocked since you wouldn't care about the overwriting if all you're doing is updating a timestamp.

Comment 3 by sharmo posted on 8/22/2006 at 3:02 AM

Nice post Ray, how about a way to track ALL site users including people who are not logged in?

I have a site where users can browse the content whether they are logged in or not and it'd be great to be able to have a display count of all the people surfing the site at any given moment, grouped by logged in users (members) and public users.

Comment 4 by Raymond Camden posted on 8/22/2006 at 2:30 PM

Roland, I do not believe a lock is needed. Since I'm using a structure, all the operations are atomic. _Maybe_ it should be used on delete. Maybe.

Sharmo: In that case you would simply store people not by their username, but by their session key. One simple way of getting this is by using session.urltoken. This is a primary key of their session and would work just fine.

Comment 5 by Roland Collins posted on 8/22/2006 at 9:49 PM

Well I guess that depends on what CF uses internally for structures...if it's a good old Java Hashtable, which is synchronized, then atomic operations should be safe. If it's anything else, then you can have problems (collisions), although rare.

Any idea what it uses?

Comment 6 by Raymond Camden posted on 8/22/2006 at 10:05 PM

coldfusion.runtime.Struct

which doesn't help us much. ;) Either way though, I'm very sure this is a safe operation.

Comment 7 by Tony posted on 8/24/2006 at 1:56 AM

I have done this before but when I applied it to our clustered production environment, each server has it own structure and I cant see all users of all servers at one time. Will this work in clustered? Any suggestions?

Comment 8 by Raymond Camden posted on 8/24/2006 at 2:03 AM

It would not work. But - what you could do is insert into a database. This seems like it may be a bit risky since data could be "forgotten" if for some reason onSessionEnd didn't fire. You would need to add a bit of "sanity" code to notice an entry that is a bit too old to be valid.

Or shoot. Don't delete at all. Log a record when they first hit, and then when they leave. This would let you do reports over time.