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:
- When a user logs on, add her to a list of logged in users.
- When a user logs out, remove her from the list.
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
Thanks Ray - this should help me out no end. You're a gentleman and a coder sir!
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.
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.
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.
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?
coldfusion.runtime.Struct
which doesn't help us much. ;) Either way though, I'm very sure this is a safe operation.
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?
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.