A reader pinged me this weekend asking about tracking sessions. I thought I'd whip up a quick blog entry demonstrating how one can count sessions with Application.cfm. (By the way, it is a lot easier with Application.cfc. I'll cover that later.) Note the stress on count. This code will only give you a count of the sessions, not data about the sessions.
Our demo will consider of two files and isn't terribly complex. The first file is the Application.cfm file.
<cfapplication name="sessioncounter" sessionManagement=true>
<!--- Do I need to create my app var? --->
<cfset needInit = false>
<cflock scope="application" type="readOnly" timeout="30">
<cfif not structKeyExists(application,"sessions")>
<cfset needInit = true>
</cfif>
</cflock>
<!--- Yes, I do need to make it. --->
<cfif needInit>
<cflock scope="application" type="exclusive" timeout="30">
<cfif not structKeyExists(application,"sessions")>
<cfset application.sessions = structNew()>
</cfif>
</cflock>
</cfif>
<!--- Store my last hit. --->
<cfset application.sessions[session.urltoken] = now()>
So what's going on here? First thing I do is define the application and enable sessions. That's kind of required if you want to - you know - count sessions.
How are we going to count sessions? In this example we are going to use a structure. ColdFusion provides a simple way to create a unique key per session - the session.urltoken variable. Note that I have about 10 lines of code here because of all the CFLOCKs. This will be a heck of a lot easier when we switch to Application.cfc. Essentially they server to ensure we only create the Application variable one time.
The last thing we do is store our session into the Application structure. This one line is pretty critical so I'll repeat it:
<cfset application.sessions[session.urltoken] = now()>
What this does is create a key based on session.urltoken, which I mentioned earlier was unique per session. The value is based on the current time. Why? Because I'm using Application.cfm and not Application.cfc, I don't know when your session will time out. By recording the time I can determine if a session is still valid.
So the next part of the puzzle is actually displaying the count. Here is my index.cfm which includes both the UDF and the display of the result. Obviously you would typically have the UDF in a separate file, but I'm combining it here for simplicity's sake.
<cffunction name="sessionCount" returnType="numeric" output="false">
<cfset var s = "">
<cfloop item="s" collection="#application.sessions#">
<cfif dateDiff("n", application.sessions[s], now()) gt 20>
<cfset structDelete(application.sessions, s)>
</cfif>
</cfloop>
<cfreturn structCount(application.sessions)>
</cffunction>
<cfoutput>
<p>
Hello. There are #sessionCount()# users on the system.
</p>
</cfoutput>
I'll focus on the UDF since the rest of the template is simply displaying the result of the UDF. So the point of this code is to return the number of sessions. Since we store a key for each session in an application structure, we could simply use the structCount function. Notice that I do indeed use this at the end of the UDF. But what about sessions that are no longer active? We need someway to filter those out. The simplest way to do this is to loop over the items in the structure and look at their value. Remember we stored their the time value of their last hit? That makes it even easier. One thing to note - I'm removing keys that are older than 20 minutes. 20 minutes is the default session timeout value. If the value is different than you would need to change that number.
Also note that I broke one of the "rules" about not using an outside scope in a UDF. Rules are never 100%, and this is a good example of that. Yes I could make this more abstract, but for a simple utility like this I say why make things overly complex?
So that's it. There are some things I want to leave you with. First - as I mentioned more than once - this is a heck of a lot simpler with Application.cfc. Seriously. So if you have CF7 (or the comparable BlueDragon version), then don't use this code. Secondly - while the UDF is rather simple, if you have a lot of sessions then the code could perform a bit slowly. You may want to consider rewriting the UDF to just return the structCount. You could then use a scheduled task to update the Application storage once every five minutes or so. This means your count is potentially a bit high, but what's a little fuzziness between friends?
Archived Comments
Nice write up Ray.
FYI: If you want to see sessions being used across your server, look here:
http://www.succor.co.uk/ind...
You'd better hope a site that uses this never get Slashdotted. Doing the "deletetion" loop inside of the sessionCount function means that every page that displays the count is also walking the array each and every time. Imagine "There are currently 153,189 users on the site.", and doing 153,189 date comparisons on every page request (not counting all of the locks and structDeletes needed).
You mentioned this in the last paragraph ("a bit slowly") but the real impact could be significant indeed.
Personally, if I had to do something like this I think I'd implement a cached query on a short fuse, doing a count on ColdFusion's "cglobal" table where lvisit was in the desired range.
Minor point, but aren't your "Do I need to create my app var?" and "Yes, I do need to make it." sections of code in the application.cfm performing a redundant check? Couldn't you eliminate the first check and just assume that we would add to the session every time EXCEPT when a duplicate SESSIONS key exists.
...wait... Did you have the redundant check because it allows you to use a 'read only' lock for the initial check?
@Dan - Yep. It minimizes the initial lock. A real pain in the rear - and much simpler in App.cfc.
@Michael - Well, maybe I didn't make it sound as dire as it should. ;) I think people get the idea though.
@Raymond - I might mention too that the cfquery method I suggested works on client scopes, not session scopes. Then again, I never use session scopes, as they're too limiting for large sites that are, or may go, multi-server.
One question: What's the rationale for the cflock inside the deletetion loop? Struct operations should be synchronized and atomic inside of MX, and if you're not in MX wouldn't you then need a cflock on the insertion side as well?
Nope, I don't think I need the lock there. I'll remove it.
I've done something like this in the past and its a fun side project. Theres a lot of additional possibilities if you app needs them
- additional layer of security if you store the session id/ip address etc. also the ability for admins to soft log someone out of your app :o
- when approaching the timeout mark you can modal a screensaver-like login form to allow them to re-authenticate without losing the work they may have had on screen.
- add a digg spy like portion showing who is online doing what but can also show idle time etc if so inclined.
its really a good first step to open up a lot of interesting things for your apps imHo (interesting as far as geeky developer looking for other things to do than routine stuff I suppose but fun nevertheless.....).
Hey Ray, Aaron (trajiklyhip.com) did a preso on session tracking (his with Application.cfc) at last years Mini-Max sessions before CFUnited. It's posted in the Presentations area of his site, and may prove useful to someone reading your post
Ray, that's pretty cool. I'm putting that into the next version of CFMBB so I can track "Current Users" and "Current Registered Users" PHPBB does that...
Hi Ray - I use this code alot for my projects, is there a way to also count visitors on site which are not logged in (using session) and add a 'guest' count?
Notice how I store the time of the last hit? You can store anything you want. Therefore you would modify the code to store their login status as well.
Thanks for the reply Ray, however I have no idea how to capture a 'visitor' who is not a member, nor logged in. Thanks again.
Err, well, it is your site, right? So you know how logins are handled. Unless I'm missing something?
I think perhaps you are. I want to add a non logged in, general site visitor to the struct, as a guest.
But what does logged in mean? That's up to _your_ web application. Maybe you use a session variable, "LoggedIn", to mark a user logged in. Maybe you use a cookie to log someone in automatically. Etc. The point is - it is your site's logic that determines if the user is logged in. I can't tell you what that is - you have to tell me. Right?
Hmm yes perhaps that's the problem. My members use session vars, I want the non session using, non logged in (with session) site visitors so I have something like Online: 23 Visitors 12 (this is looking like a baseball score I know...) Once we get some place with this please feel free to delete all the comments, might bore the crap outta someone :)
Well, you need to look at your code. Which variable do you use to mark them logged in? Ie, what specific session variable? If is session.loggedin, then you use that along with what I said above. You record that to the application scope structure.
Yep, this works perfectly fine, I've modified this code many times/places/uses to also store userID,userDisplayName etc, however, at the risk of annoying you Ray, I'm wanting to add guests to the struct, by guests I mean, not logged in, non members, who are simply browsing the the site.
Anyone who visits your site has a session, even if they don't log on. Notice how my code just adds the time? Well when you add in your data, if the logged in key is missing, you add in a flag marking the user as a guest.
AHA! Ty Jedi.