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
- Counts the number of visitors to your site
- Counts how many times each page is accessed
- Records how long each page is looked at
- Can optionally store the statistics in SQL Server
- ...or cache them in arrays in memory
- ...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
- 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.
- 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.
|