Logo: TechTrax...brought to you by MouseTrax Computing Solutions

Web Tracking Statistics

by Adrian Forbes, MVP
Skill rating level 11.

It is always good to get a rough idea about how much your site is being used, what pages are being used more than others etc. There are many web statistic tracking services on the web already which are great at what they do and can supply you with graphs on your site usage etc. But if you want something a little more tailored or something that doesn't require you to add a third party's code to every page you want to track it isn't that hard to set some form of system up yourself. This article is going to cover the basics.

Features

  1. Counts the number of visitors to your site
  2. Counts how many times each page is accessed
  3. Records how long each page is looked at
  4. Can optionally store the statistics in SQL Server
  5. ...or cache them in arrays in memory
  6. ...or cache them in XML in memory

Either memory-based cache can be periodically written to SQL Server or to a file on disc so that the data isn't lost on a reboot. If you have persisted statistics in SQL Server or in a file then they are loaded up to carry on after a reboot or similar.

In order to capture how many people visit the site we'll utilise the built-in events given to us by IIS. The running count will be held in the Application object and will be incremented on each Session_onStart. In the global.asa file create the Application_onStart event if it isn't already there and add the following code;

Sub Application_OnStart ()
  Application("ConnectionString") = "provider=sqloledb;server=localhost;database=HitCount;uid=sa;pwd=;"   Application("HitCount") = 0
End Sub

Now do something similar with the Session_onStart event;

Sub Session_OnStart ()
  Application.Lock
  Application("HitCount") = Application("HitCount") + 1 Application.Unlock End Sub

Note that before we update the Application object we lock it using the Lock method. This means that no other processes can update the Application until our process unlocks it. ASP is a multi-user environment and other instances of the page may be running for other users. If we didn't lock the Application then it might lead to unpredictable code. It is a general rule that you lock the Application if you want to update it (reading is fine), then unlock it when you have finished writing. As with all locks, try to make them as short as possible.

Well that was pretty easy. The process of capturing each page needs slightly more work. We'll implement a solution by having our code in a separate file and we'll use the INCLUDE directive to include the code in each page that we are interested in, or all pages if you so desire. As mentioned in the intro there will be three methods of capturing the data so we'll look at each three in turn.

In order to work out how long a page has been looked at we'll record in the Session when a user looks at a page and the time they looked at it. When they look at another page we examine the time difference between the current time and the time the last page was looked at. So for each page we will record that page's hit, and we'll update the time information for the previous page.

In this example we are recoding the time information in seconds, but you may want to use minutes instead.

Capturing to SQL Server is the easiest method as SQL Server will be holding all of the "state", i.e., the statistics. We don't need our ASP code to really hold anything other than the connection string. The database table has three columns; the name of the page, the number of times it has been accessed and the length of time someone has looked at it.

CREATE TABLE [dbo].[HitCount] (
  [PageName] [varchar] (255) NOT NULL ,
  [HitCount] [int] NOT NULL ,
  [Seconds] [int] NOT NULL 
) ON [PRIMARY]
GO

We're going to use a stored procedure (SP) to record each hit and we'll build most of our logic into that procedure to make our ASP coding as simple as possible. The SP accepts the page name, the previous page name and time data as parameters and checks to see if the page already exists in the table. If it doesn't then we create a new row for it. If it already exists we increment it's count and it's time data. If the name of the previous page has been passed in then we increment that page's time data.

CREATE PROCEDURE HitRegister
@PageName varchar(255),
@LastPage varchar(255) = null,
@Seconds int = null
AS

IF EXISTS (
  SELECT 1 FROM HitCount WHERE PageName = @PageName)
BEGIN   UPDATE     HitCount   SET HitCount = HitCount + 1   WHERE     PageName = @PageName
END ELSE BEGIN   INSERT INTO     HitCount     (       PageName,       HitCount,       Seconds     )   VALUES     (       @PageName,       1,       0     )
END IF @LastPage IS NOT NULL   UPDATE     HitCount   SET Seconds = Seconds + @Seconds   WHERE     PageName = @LastPage

As this SP is self-contained, we simply call it with the relevant parameters and it does all of our work.

The connection string to the database will be held in the Application object so add this code to the Application_OnStart event;

Application("ConnectionString") = "provider=sqloledb;server=;database=;uid=;pwd=;"

Change this string to reflect the connection to your database that is holding the HitCount table.

The file we are going to include needs to call our SP with the name of the current page, then record the time and name of the current page in the Session so that next time around we can work out the time difference.

PageCounterSQL.asp

<!--#include file="../../2004_03_March/Adrian_HitCount/constants.asp"--%gt;
<%
sFile = Request.ServerVariables("SCRIPT_NAME")
sLastPage = Session("LastPage")
set objCom = Server.CreateObject("ADODB.Command")
With objCom
  .CommandType = 4
  .CommandText = "HitRegister"
  .ActiveConnection = Application("ConnectionString")
  .Parameters.Append .CreateParameter("PageName", adVarChar, 1, 255, sFile)
  if sLastPage <> "" then
    lSeconds = DateDiff("s", Session("LastAccess"), Now)
    .Parameters.Append .CreateParameter("LastPage", adVarChar, 1, 255, sLastPage)
    .Parameters.Append .CreateParameter("Seconds", adInteger, 1, 4, lSeconds)
  end if
  .Execute
  .ActiveConnection.Close
end with
set objCom = nothing

Session("LastPage") = sFile
Session("LastAccess") = Now
%>

Note that the first thing we do is include a file call constants.asp. This is a file that will hold various constants for us. We get the name of the current page from the ServerVariables collection. This includes path information so "/folder1/page.asp" won't be confused with "/folder5/mystuff/page.asp". It also excludes any QueryString info so "page.asp?do=something" will come back as only "page.asp".

We then retrieve the name of the last page from the Session object. We create a Command object for called the HitCount SP. We add the PageName parameter and if the last page has come back non-empty we work out the time difference and add the relevant parameters. The reason we need to check to see if the last page is empty is in case this is the first page the user has looked at.

The constants file looks like this;

<%
adVarChar = 200
adInteger = 4
%>

Note that we are enclosing our include files with <%  %> delimeters. This is so that someone entering the location of our include files direct into the browser won't return any code to them. It also means that we need to INCLUDE our files outside of any existing <%  %> block. Like so;

<%
' some code
%>
<!--#include file="../../2004_03_March/Adrian_HitCount/constants.asp"-->
<%
' more code
%>

That is all that is needed to log the data so we need a page that let's us view our statistics, and another SP to bring the statistics back.

CREATE PROCEDURE HitsList
AS

SELECT
  PageName,
  HitCount,
  Seconds
FROM
  HitCount
ORDER BY
  HitCount DESC

StatsSQL.asp

<%@ Language=VBScript %>
<html>
<head>
<meta name="GENERATOR" content="Microsoft Visual Studio 6.0">
</head>
<body>

<!--#include file="../../2004_03_March/Adrian_HitCount/PageCounterSQL.asp"-->

<table border="1">
<%
set objRS = Server.CreateObject("ADODB.Recordset")
objRS.Open "HitsList", Application("ConnectionString")
while not objRS.EOF
%>
<tr>
<td><%=objRS("PageName")%></td>
<td><%=objRS("HitCount")%></td>
<td>
<%=formatdatetime(dateadd("s", objRS("Seconds"), cdate("00:00")), 4)%> (<%=objRS("Seconds") mod 60%>)</td>
</tr>
<%
  objRS.MoveNext
wend
objRS.Close
set objRS = nothing
%>
</table>
</body>
</html>

We need to INCLUDE this file in any page we want to track. Note the rather complicated way of showing the time information. It converts the seconds into a HH:MM format by adding the number of seconds onto "00:00" and shows that in your system's short time format. Any remainder seconds are then shown in parenthesis afterwards.

Page1.asp

<%@ Language=VBScript %>
<html>
<head>
<meta name="GENERATOR" content="Microsoft Visual Studio 6.0">
</head>
<body>

<!--#include file="../../2004_03_March/Adrian_HitCount/PageCounterSQL.asp"-->

<p>Page 1

</body>
</html>

The downside with this method is that every page needs a database hit. If someone is viewing your pages for a long time it might fail to record the time information correctly. If your Session timeout is 20 mins and someone starts to look at your page then it is record in the Session. If they look at your page for 25 minutes then their Session has timed out and the fact that they were looking at your page is lost. When they look at another page they get a new, empty Session object.

Instead of hitting the database on every page we could store the data in the Application object instead. We'll look at doing this with arrays and also in XML depending on what grills your burgers. Personally I can't stand arrays.

We'll be storing the data in a multi-dimensional array and in order to remember what position each element is as we'll be using constants. So update constants.asp;

<%
PageIndex = 0
CountIndex = 1
SecondIndex = 2
adVarChar = 200
adInteger = 4
%>

In the SQL system we used the SP to control our data but now we're going to have to do some more work in the ASP code. The basic principal is the same; work out if the page already exists in our array and if it does we'll update it, if it doesn't we'll add it. The array will have three dimensions, one for the page name, one for the hit count and one for the time data. The array will be stored in the Application object so we need to initialise this in our Application_OnStart event;

Dim aPages(2, 0)
Application("HitArray") = aPages

This will give us an array with one empty element in it. We won't ever use this element, however, but we need to create the array before we use it. When you are working with arrays and the Application or Session object we need to retrieve the array into a local variable before we use it. Code like this won't work;

Application("MyArray")(5) = "Hello"

We have to manipulate the array locally.

aMyArray = Application("MyArray")
aMyArray(5) = "Hello"
Application("MyArray") = aMyArray

An explanation for this is included in the footnotes (1) for anyone who is interested, but we'll press on for now.

This is the code for our array-based hit counter;

PageCounterArray.asp

<%
sFile = request.servervariables("SCRIPT_NAME")

Application.Lock
aPages = Application("HitArray")

if Session("LastPage") <> "" and IsDate(Session("LastAccess")) then
  sLastPage = Session("LastPage")
  for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex)
    if aPages(PageIndex, i) = sLastPage then
      lSeconds = Clng(aPages(SecondIndex, i))
      lSeconds = lSeconds + DateDiff("s", Session("LastAccess"), Now)
      aPages(SecondIndex, i) = lSeconds
      exit for
    end if
  next
end if

Session("LastPage") = sFile
Session("LastAccess") = Now

bFound = false
for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex)
  if aPages(PageIndex, i) = sFile then     aPages(CountIndex, i) = aPages(CountIndex, i) + 1     bFound = true     exit for   end if
next if not bFound then   redim preserve aPages(SecondIndex, ubound(aPages, SecondIndex) + 1)   aPages(PageIndex, ubound(aPages, SecondIndex)) = sFile   aPages(CountIndex, ubound(aPages, SecondIndex)) = 1   aPages(SecondIndex, ubound(aPages, SecondIndex)) = 0 end if Application("HitArray") = aPages %>

We work out the time difference as before then search for the previous page in the array and update it. Next we look for the existing page and update that, or add it if the array can't be found. The array is structured like so;

(0, 1) - page name for first row
(1, 1) - hit count for first row
(2, 1) - time info for first row
(0, 2) - page name for second row
(1, 2) - hit count for second row
(2, 2) - time info for second row

In order to loop through all arrays we use

for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex)

To find the upper bound of the array we need to specify that we want the bound on the last element in the array (the time info which is index 2 however we use our SecondIndex constants in the code. Note that "SecondIndex" refers to the index for the time/seconds dimension, not the numerically second index). The reason we add one to the lower bound is because we are not using the first row in our array.

Every time we add to the row we need to re-dimension the array;

ReDim Preserve aPages(SecondIndex, ubound(aPages, SecondIndex) + 1)

Again all operations need to be done on the last element (SecondIndex) so we used that in our Ubound calculation. We want to ReDim the array to be one bigger than the current upper bound which is what the above statement does.

We need a different page to view the statistics from this array.

StatsArray.asp

<%@ Language=VBScript %>
<html>
<head>
<meta name="GENERATOR" content="Microsoft Visual Studio 6.0">
</head>
<body>
<!--#include file="../../2004_03_March/Adrian_HitCount/PageCounterArray.asp"-->

<table border="1">
  <tr>
    <td>
      Session count</td><td colspan=2>
      <%=Application("HitCount")%>
    </td>   </tr> <% for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex) %>   <tr>     <td>
      <%=aPages(PageIndex, i)%>     </td>
    <td>       <%=aPages(CountIndex, i)%>     </td>     <td>       <%=formatdatetime(dateadd("s", aPages(SecondIndex, i), cdate("00:00")), 4)%>       (<%=aPages(SecondIndex, i) mod 60%>)
    </td>   </tr> <% next %> </table> </body> </html>

The advantage of this method is that the statistics are held in memory so we don't need to hit SQL Server each time. The down side is that an Application restart will lose all of your data. We'll address this problem later on.

Here is the same system only using XML. The XML we are storing looks like this;

<pages>
  <page count="5" time="25">page1.asp</page>
  <page count="8" time="173">page2.asp</page> </pages>

The advantage of XML over arrays is that it is easier to manipulate as there is a set of COM objects that let us manipulate XML. The object in question is the MSXML.DOMDocument object. We could use this object to hold the XML and store it in the Application object but we won't as you shouldn't store COM objects in the Application unless they support the appropriate threading model. If you are not 100% sure of your object's threading behaviour you should play safe and keep it out of the Application or Session. If you are interested why then I have explained in the Footnotes (2). Instead we'll store the XML in the Application object as a string and create an instance of MSXML.DOMDocument and load the string into it when we need to manipulate it.

Add this code to your Application_OnStart event;

Application("HitXML") = "<pages/>"

<P>This is an empty Pages node that we'll add to with our page information.

<P>The XML counter looks like this;

<pre>
PageCounterXML.asp
<%
sFile = request.servervariables("SCRIPT_NAME")

Set objXML = Server.CreateObject("MSXML.DOMDocument")

Application.Lock
objXML.loadXML Application("HitXML")
Set objNode = objXML.selectSingleNode("/Pages/Page[.='" & sFile & "']")

If objNode Is Nothing Then
  Set objRoot = objXML.selectSingleNode("/Pages")
  objRoot.appendChild CreateNode (sFile)
  set objRoot = nothing
Else
  objNode.Attributes.getNamedItem("Count").nodeValue = objNode.Attributes.getNamedItem("Count").nodeValue + 1
End If
set objNode = nothing

if Session("LastPage") <> "" and IsDate(Session("LastAccess")) then
  Set objNode = objXML.selectSingleNode("/Pages/Page[.='" & Session("LastPage") & "']")
    if not objNode is nothing then
    lSeconds = Clng(objNode.Attributes.getNamedItem("Time").nodeValue)
    lSeconds = lSeconds + DateDiff("s", Session("LastAccess"), Now)
    objNode.Attributes.getNamedItem("Time").nodeValue = lSeconds
  end if
end if

Application("HitXML") = objXMl.xml

Session("LastPage") = sFile
Session("LastAccess") = Now

'Response.write "<p>" & Server.HTMLEncode(objXML.xml) & "</p>"

function CreateNode (NodeName)
  dim objNewNode

    Set objNewNode = objXML.createElement("Page")
    objNewNode.Text = sFile
    Set objAtt = objXML.createAttribute("Count")
    objAtt.Value = "1"
    objNewNode.Attributes.setNamedItem objAtt
    Set objAtt = objXML.createAttribute("Time")
    objAtt.Value = "0"
    objNewNode.Attributes.setNamedItem objAtt
    set CreateNode = objNewNode
    set objNewNode = nothing
end function
%>

We create an instance of DOMDocument and load in our XML string from the Application. Rather than having to loop through our XML trying to find a page entry we use Xpath searching;

Set objNode = objXML.selectSingleNode("/Pages/Page[.='page.asp']")

This will return a node called Page that is a child of the Pages root node if that Page node has our page name in it's text. So such a node would be found;

<page count="2" time="10">page.asp</page>

Once the node has been found we just read/write the Count and Time nodes with our data, again using the COM objects contained in MSXML. When we are done with the XML that is inside the DOMDocument we use the Xml property to return us the XML as simple text which we store back in the Application. This also needs a different page to view the stats;

StatsXML.asp

<%@ Language=VBScript %>
<html>
<head>
<meta name="GENERATOR" content="Microsoft Visual Studio 6.0">
</head>
<body>
<!--#include file="../../2004_03_March/Adrian_HitCount/PageCounterXML.asp"-->

<table border="1">
<tr>
<td>Application started</td><td colspan=2>
<%=Application("StartDate")%></td>
</tr>
<tr>
<td>Session count</td><td colspan=2>
<%=Application("HitCount")%></td>
</tr>
<%
Set objXML = Server.CreateObject("MSXML.DOMDocument")

objXML.loadXML Application("HitXML")

Set objNodeList = objXML.selectNodes("/Pages/Page")
for each objNode in objNodeList
	lSeconds = Clng(objNode.Attributes.getNamedItem("Time").nodeValue)
%>
<tr>
<td><%=objNode.text%></td><td>
<%=objNode.Attributes.getNamedItem("Count").nodeValue%>
</td><td>
<%=formatdatetime(dateadd("s", lSeconds, cdate("00:00")), 4)%> 
(<%=lSeconds mod 60%>)</td>
</tr>
<%
next
%>
</table>

<p>Last saved at <%=Application("LastSave")%>, next save at <%=Application("NextSave")%></p>

<p><a href="../../2004_03_March/Adrian_HitCount/default.asp">Main menu</a></p>

</body>
</html>

The downside of these Application-based routines is that the data lasts only as long as the Application object does. To get the best of both worlds we can periodically persist the data to somewhere permanent like a text file or database. To achieve this we will hold some more information in the Application object, the time the data was last saved, the time it will be saved next, and the time period to wait between saves. So amend the Application_onStart event;

Application("SavePeriod") = 1 ' number of minutes
Application("LastSave") = Now
Application("NextSave") = DateAdd("n", Application("SavePeriod"), Now())

Now we need to amend the counter pages for both the array and XML methods to check to see if this has passed and the data should be saved. If the date has passed then we'll save the data to a text file. After the data is saved we'll set the required time of the next save. Append this code to the bottom of PageCounterArray.asp;

<!--#include file="../../2004_03_March/Adrian_HitCount/constants.asp"-->
<%
sFile = request.servervariables("SCRIPT_NAME")

Application("HitArray") = aPages

.....

If CDate(Application("NextSave")) < Now then

  Set objFSO = Server.CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.CreateTextFile(Server.MapPath ("stats.txt"), True)
  objFile.WriteLine "Session count"
  objFile.WriteLine Application("HitCount")
  for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex)
    sPageName = Replace(aPages(PageIndex, i), "'", "''")
    lHitCount = Clng(aPages(CountIndex, i))
    lSeconds = Clng(aPages(SecondIndex, i))
    objFile.WriteLine sPageName
    objFile.WriteLine lHitCount
    objFile.WriteLine lSeconds
  next
  objFile.Close
  Set objFile = Nothing
  Set objFSO = nothing

  Application("LastSave") = Now
  Application("NextSave") = DateAdd("n", Application("SavePeriod"), Now)
end if Application.Unlock %>

We write the Session count first and then each page and its data. The format is very simple with one bit of data on each line. So if page.asp has been viewed 10 times for 30 seconds then we write

Page.asp
10
30

If you want to do something more meaningful then you can do that. The reason I have chosen such a basic format will become clear later. We can do something similar for the XML method.

<%
sFile = request.servervariables("SCRIPT_NAME")

Set objXML = Server.CreateObject("MSXML.DOMDocument")

....

Session("LastPage") = sFile
Session("LastAccess") = Now

If CDate(Application("NextSave")) < Now then
  Set objNodeList = objXML.selectNodes("/Pages/Page")
  Set objFSO = Server.CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.CreateTextFile(Server.MapPath ("stats.txt"), True)
  objFile.WriteLine "Session count"
  objFile.WriteLine Application("HitCount")
  for each objNode in objNodeList
    sPageName = Replace(objNode.Text, "'", "''")
    lHitCount = Clng(objNode.Attributes.getNamedItem("Count").nodeValue)
    lSeconds = Clng(objNode.Attributes.getNamedItem("Time").nodeValue)
    objFile.WriteLine sPageName
    objFile.WriteLine lHitCount
    objFile.WriteLine lSeconds
  next
  objFile.Close
  Set objFile = Nothing
  Set objFSO = nothing
  Set objNodeList = nothing

  Application("LastSave") = Now
  Application("NextSave") = DateAdd("n", Application("SavePeriod"), Now)
end if
Application.Unlock

function CreateNode (NodeName)

....

%>

As well as saving to a file we could save to a database. Seeing as we have already designed a table to hold the hits we'll re-use that. The only SPs we have so far are one to list the hits and one to register a hit. If we are to persist this data to that table we need a new SP that will allow us to explicitly set all the values in the table.

CREATE PROCEDURE HitCountSet
@PageName varchar(255),
@HitCount int,
@Seconds int
AS

IF EXISTS(
  SELECT 1 FROM HitCount WHERE PageName = @PageName) BEGIN
  UPDATE
    HitCount
  SET
    HitCount = @HitCount,
    Seconds = @Seconds
  WHERE
    PageName = @PageName

END ELSE BEGIN

  INSERT INTO
    HitCount
    (
      PageName,
      HitCount,
      Seconds
    )
  VALUES
    (
      @PageName,
      @HitCount,
      @Seconds
    )
END

Now let's amend the PageCounterArray.asp and PageCounterXML.asp pages.

PageCounterArray.asp

<!--#include file="../../2004_03_March/Adrian_HitCount/constants.asp"-->
<%
sFile = request.servervariables("SCRIPT_NAME")

....

If CDate(Application("NextSave")) < Now then

  set objCon = Server.CreateObject("ADODB.Connection")
  objCon.Open Application("ConnectionString")
  objCon.Execute "HitCountSet 'Session count', " & Application("HitCount") & ", 0"
  for i = lbound(aPages, SecondIndex) + 1 to ubound(aPages, SecondIndex)
    sPageName = Replace(aPages(PageIndex, i), "'", "''")
    lHitCount = Clng(aPages(CountIndex, i))
    lSeconds = Clng(aPages(SecondIndex, i))
    objCon.Execute "HitCountSet '" & sPageName & "', " & lHitCount & ", " & lSeconds
  next
  objCon.Close
  set objCon = nothing

  Application("LastSave") = Now
  Application("NextSave") = DateAdd("n", Application("SavePeriod"), Now)
end if
Application.Unlock

%>

We are using a Connection object to call the SP as we need to call it multiple times using the same connection.

PageCounterXML.asp

<%
sFile = request.servervariables("SCRIPT_NAME")

Set objXML = Server.CreateObject("MSXML.DOMDocument")

....

If CDate(Application("NextSave")) < Now then
  Set objNodeList = objXML.selectNodes("/Pages/Page")
  Set objCon = Server.CreateObject("ADODB.Connection")
  objCon.Open Application("ConnectionString")
  objCon.Execute "HitCountSet 'Session count', " & Application("HitCount") & ", 0"
  for each objNode in objNodeList
    sPageName = Replace(objNode.Text, "'", "''")
    lHitCount = Clng(objNode.Attributes.getNamedItem("Count").nodeValue)
    lSeconds = Clng(objNode.Attributes.getNamedItem("Time").nodeValue)
    objCon.Execute "HitCountSet '" & sPageName & "', " & lHitCount & ", " & lSeconds
  next
  objCon.Close
  set objCon = nothing
  set objNodeList = nothing

  Application("LastSave") = Now
  Application("NextSave") = DateAdd("n", Application("SavePeriod"), Now)
end if

Application.Unlock

....

%>

So now we are storing the hit count data in the Application object and periodically saving it out to disc or a database, the only thing left to do is to load that data back in when the Application starts up. This lets us retain our statistics and start where we left off after a re-boot. This will be done in the Application_OnStart event. For arrays saved to the database;

  dim aPages()
  set objRS = Server.CreateObject("ADODB.Recordset")
  objRS.CursorLocation = 3
  objRS.Open "HitsList", Application("ConnectionString")
  if objRS.EOF then
    redim aPages(2, 0)
  else
    redim aPages(2, objRS.RecordCount - 1)
    i = 0
    while not objRS.EOF
      if strcomp(objRS("PageName"), "Session count", 1) = 0 then
        Application("HitCount") = Clng(objRS("HitCount"))
      else
        i = i + 1
        aPages(0, i) = objRS("PageName")
        aPages(1, i) = objRS("HitCount")
        aPages(2, i) = objRS("Seconds")
        objRoot.appendChild CreateNode (objXML, objRS("PageName"), _&
        objRS("HitCount"), objRS("Seconds"))
      end if
      objRS.MoveNext
    wend
  end if
  objRS.Close
  Application("HitArray") = aPages

For arrays saved to disc;

  redim aPages(2, 0)
  set objFSO = Server.CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.OpenTextFile(Server.MapPath ("stats.txt"), 1, True)
  while not objFile.AtEndOfStream
    sPageName = objFile.ReadLine
    lHitCount = clng(objFile.ReadLine)
    if strcomp(sPageName, "Session count", 1) = 0 then
      Application("HitCount") = lHitCount
    else
      i = i + 1
      lSeconds = clng(objFile.ReadLine)
      redim preserve aPages(2, ubound(aPages, 2) + 1)
      aPages(0, i) = sPageName
      aPages(1, i) = lHitCount
      aPages(2, i) = lSeconds
      objRoot.appendChild CreateNode (objXML, sPageName, lHitCount, lSeconds)
    end if				
  wend
  Application("HitArray") = aPages
    

For XML saved to the database;

   Set objXML = Server.CreateObject("MSXML.DOMDocument")

  objXML.loadXML Application("HitXML")   Set objRoot = objXML.selectSingleNode("/Pages")   dim aPages()   set objRS = Server.CreateObject("ADODB.Recordset")   objRS.CursorLocation = 3   objRS.Open "HitsList", Application("ConnectionString")   if objRS.EOF then     redim aPages(2, 0)   else     redim aPages(2, objRS.RecordCount - 1)     i = 0     while not objRS.EOF       if strcomp(objRS("PageName"), "Session count", 1) = 0 then         Application("HitCount") = Clng(objRS("HitCount"))       else         i = i + 1         aPages(0, i) = objRS("PageName")         aPages(1, i) = objRS("HitCount")         aPages(2, i) = objRS("Seconds")         objRoot.appendChild CreateNode (objXML, objRS("PageName"), & _         objRS("HitCount"), objRS("Seconds"))       end if       objRS.MoveNext     wend   end if   objRS.Close   Application("HitXML") = objXML.xml

For XML saved to disc;

  redim aPages(2, 0)
  Set objFSO = Server.CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.OpenTextFile(Server.MapPath ("stats.txt"), 1, True)
  while not objFile.AtEndOfStream
    sPageName = objFile.ReadLine
    lHitCount = clng(objFile.ReadLine)
    if strcomp(sPageName, "Session count", 1) = 0 then
      Application("HitCount") = lHitCount
    else
      i = i + 1
      lSeconds = clng(objFile.ReadLine)
      redim preserve aPages(2, ubound(aPages, 2) + 1)
      aPages(0, i) = sPageName
      aPages(1, i) = lHitCount
      aPages(2, i) = lSeconds
      objRoot.appendChild CreateNode (objXML, sPageName, lHitCount, lSeconds)
    end if				
  wend
  Application("HitXML") = objXML.xml

By saving the data to disc in such a simple format we can easily read it back. As the various systems all use the same database and concepts I have included a set of demo pages that has all of the above code and a menu system to let you use the SQL Server, Array or XML methods of recording the counts and lets you choose to persist the data to disc or database.

Footnotes

  1. The reason we need to do this is because the Application object has a default collection called Contents. When you omit a method name from an object then COM thinks you want the default method so the parenthesis is applied to the default method. For example "Fields" is the default method for the recordset object so
        objRS("MyField")
    
    Is really
        objRS.Fields("MyField")
    
    COM inserts the ".Fields" for us. So when you write this code;
        Application("MyArray")
    
    You are actually using
        Application.Contents("MyArray")
    
    So when you code Application("MyArray")(5) it assumes ("MyArray")(5) was for the default method, Contents, which confuses it. If you want to use an array direct from the Application or Session you should explicitly use the Contents collection.
        Application.Contents("MyArray")(5).
    

    Note that Microsoft have patched the code such that later versions of script can indeed use Application("MyArray")(5) directly, but if you want your code to run on older versions of the script engine you should avoid doing this. It is also more efficient to hold the array in a local variable, otherwise you're searching the whole Application object for "MyArray" each time you want to use it.

  2. Some threading models have a concept known as "thread affinity" where only the thread that creates the object can access the object. If you store such an object in the Application or Session object then only that thread can access it. So if I am using Thread 1 when I create the object and store it in the Application, and later on I am on Thread 4 and I want to use it, I need to wait until Thread 1becomes available again. This can degrade the performance of your site, especially if it is busy.

Click to rate this article.

 

Go up to the top of this page.
This site powered by the Logical Web Publisher™: Content management by Logical Expressions, Inc.