You are on page 1of 11

Page Tracking in ASP.

NET
By vikramk | 23 Jun 2004
This articles explains how to track visitors to your web site.
Top of Form
/w EPDw UKMTAy

• Download demo project - 32 Kb


Introduction
This article explains how to track ASP.NET web pages through your own web application
without relying on external tracking tools. This does not eliminate the need for external tools, but
it gives some satisfaction to have your own tracking that you can play with and modify to suit to
your business requirements. This is all possible due to the rich API provided by the .NET
framework.
I researched on the web to find out if people have developed a code to do custom tracking in
.NET, and I found an excellent article by Wayne Plourde that explains how to track web pages in
ASP.NET. This article derives lots of inspiration from Wayne Plourde's original article. I
recommend that you read this article first as it will help you to understand page tracking
concepts.
When I ported Wayne Plourde's VB.NET code to C#, I stumbled upon a few problems regarding
session state and the way it is being handled in .NET framework by Microsoft architects. I have
been thinking for a long time to write on this subject and share my thoughts with the CodeProject
community. Now I got an excellent opportunity to share my thoughts as I am switching jobs.
When I publicized my resume for a new job, I wanted to see real time usage of web page
tracking methods. I was thrilled to see that people spend an average of 2-10 seconds to see a
resume, and if it does not catch their attention, it is buried forever.
Location - Location - Location
As a web application developer, I am interested to know the location of the visitors coming to a
web site. This is important from business standpoint to target your audience accordingly. In a
simple scenario, I am interested to know the location of people accessing my resume just for the
sake of curiosity.
There are some articles on CodeProject on geographical locations of the visitors based upon an
IP address. I like GeoBytes as they provide free service for non-commercial use. GeoBytes
provide a simple cut-and-paste code that you can put on your site to greet your users.
I want little more on this, as I need to store geographical location of the users based upon an IP
address for statistical analysis on the pattern of visitors. Currently, GeoBytes does not provide a
web-service like method in which you pass an IP address from your web server and it returns the
geographical location. There is a little work-around to this.
• Make a startup page say index.html.

Collapse
<html>
<body>
<script language="Javascript"
src="http://gd.geobytes.com/Gd?after=-
1&variables=GeobytesCountry,GeobytesCity,GeobytesRegion">
</script>
<script language="javascript">
if(typeof(sGeobytesCountry) != "undefined" &&
typeof(sGeobytesRegion) != "undefined" &&
typeof(sGeobytesCity) != "undefined")
{
var url = 'index.aspx?IPCity='+sGeobytesCity+'&IPRegion='+
sGeobytesRegion+'&IPCountry='+sGeobytesCountry;
document.write("<META HTTP-EQUIV='Refresh' CONTENT='0;
URL="+url+"'>");
} else
{
document.write("<META HTTP-EQUIV='Refresh' CONTENT='0;
URL=index.aspx'>");
}
</script>
</body>
</html>
• Through the above startup HTML page, a trip is made to GeoBytes site and it returns the
city, region, and country based upon an IP address in HTTP request. After this, we call
our regular ASPX startup page and pass these three parameters as Request query string.
Now you have the information, and use it to analyze or redirect the users to custom pages
based upon their locations. For non-commercial use, this may be an acceptable solution,
but for high volume sites, you need to have your own geo database.
• For demo project to work on your machine, please make sure that you define index.html
as a default document type through your inetmgr MMC.
Issues with Session State
If you are developing a scalable web application in ASP.NET, you will come across a need to
switch your session state from InProc to either StateServer or SQLServer in the future. In my
early stages of learning ASP.NET, we always used InProc session state, but later we had to
change it to StateServer from scalability point of view. There is an excellent FAQ on Session
State by Patrick Y. Ng in the ASP.NET site, and it is worth spending some time to read this
article to get an idea about session state in detail.
If your application uses InProc session state, Wayne Plourde's article code is sufficient enough
to do the page tracking. I had to do some changes in the concepts of web page tracking to
accommodate StateServer session state, due to following major reasons:
• Session_End event is not supported in StateServer or SQLServer Session mode.
• Objects need to be serializable when using StateServer or SQLServer Session mode.
This article's code works for all three session states, and you can make this code a part of your
own framework and enable page tracking by having a few entries in your web.config file.
ASP.NET Page Tracking
My purpose was not to invent how ASP.NET page tracking should be done. I was just trying to
make Wayne's code work for all three session states. I have explained the changes that I had to
make in order for it to work for the limitations explained above.
• Simulate Session_End event in StateServer or SQLServer Session mode by creating a
cache object with a hookup that will fire exactly on session time out.
• This approach has its own caveat as the session objects will not be available when we
simulate session_end event, as ASP.NET will recycle the session at that very moment
when we were trying to access it.
• We simulate the session object by creating a simple static hash table that will hold our
objects until the session ends.
Basics of Wayne's Page Tracking
• Create a Session Tracker class.
• Persist this class in a ASP.NET Session.
• Use cookies to store previous values of variables that we are interested to track.
• Update the tracker class in Session whenever user visits a page.
• Publish the data when session_start and session_end events fire.
• Show a summary of Page Visited Statistics when session expires.
Modifications to Wayne's Page Tracking for All Three Session States
• Create a modified Session Tracker Class.
• Simulate a session_end event when session expires.
• Persist this class in a static hash table in view of session_end event problem.
• Update the tracker class in static hash table whenever user visits a page.
• Publish the data when session_start and session_end events fire.
• Show a summary of Page Visited Statistics when session expires.
Global.asax v/s HttpModule
The global.asax.cs provides methods that execute on specific events. Each web application has
its own global.asax.cs file, but it is my own personal taste not to use this file and instead use
HTTP Module due to the following reasons:
• Http Module can be used by many web applications that you develop.
• There is no need to copy code from one global.asax web application to another web
application when it is going to be same for all web applications.
• Keep a common code in your own framework and use that framework all across your
web applications.
Register your HttpModule
In the demo project, I have a small framework class library called EADFramework. EAD stands
for Enterprise Application Development. Reference this class library in your web project.
Modify web.config to register HttpModule and set parameters used by Page Tracking.

Collapse
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="MailFrom"
value="mailto:yourMailFrom@yourserver.com">yourMailFrom@yourserver.com" />
<add key="MailTo"
value="mailto:yourMailTo@yourserver.com">yourMailTo@yourserver.com" />
<add key="SmtpServer" value="smtp.yourmailserver.com" />
<add key="SiteTracking" value="Full" />
<add key="ExcludeIPList" value="127.0.0.1,10.,192.168,172.16" />
</appSettings>
<system.web>
<httpModules>
<add name="EADHttpModule"
type="EAD.Controller.EADHttpModule, EADFramework" />
</httpModules>
<compilation defaultLanguage="c#" debug="true" />
<trace enabled="false" requestLimit="10" pageOutput="false"
traceMode="SortByTime" localOnly="true" />
<sessionState mode="StateServer"
stateConnectionString="tcpip=127.0.0.1:42424" timeout="20" />
</system.web>
</configuration>
You can turn on/off site tracking by using SiteTracking value as "Full" or "None". In my own
framework, I also use "Partial" tracking to track selected pages only. This itself is a separate
topic for navigation to/from pages. I use a separate class for Page Navigation using attributes
defined in web.config. One of the attributes for Page Navigation is PageVisit and when set on
'true', it turns page tracking on for that page. For the purpose of simplicity, I am not putting the
code for partial web page tracking. I used the PageVisit method in PageTrackerUtil class to
control partial page tracking.
You can specify ExcludeIPList to filter out your local or internal IPs for tracking. You could
monitor one or more specific IP by using IncludeIPList. Use IncludeUserList and
ExcludeUserList to list CSV of users that need to be monitored or excluded from monitoring.
The session state is defined as a StateServer. When you run this demo, please make sure that
your ASP.NET State Service is running on your web server.
Define Events for HttpModule
We need three events to register in our HttpModule for page tracking.
• Register Events:

Collapse
public void Init(HttpApplication app)
{
app.BeginRequest += (new EventHandler(this.OnBeginRequest));
app.PreRequestHandlerExecute +=
(new EventHandler(this.OnPreRequestHandlerExecute));
if (app.Modules["Session"] != null)
{
SessionStateModule session = (SessionStateModule)
app.Modules["Session"];
app.AcquireRequestState +=
(new EventHandler(this.OnAcquireRequestState));
session.Start += (new EventHandler(this.OnSessionStart));
}
}
• AcquireRequestState event - This event occurs when ASP.NET acquires the current
session state associated with the current request. This is an ideal place to create a cache
object with a callback function. This callback function timeout is set to session timeout
limit.

Collapse
private void OnAcquireRequestState(Object source, EventArgs ea)
{
HttpApplication app = (HttpApplication)source;
HttpContext ctx = (HttpContext) app.Context;
string key = ctx.Session.SessionID;
CacheItemRemovedCallback onCacheRemove = null;
try
{
ctx.Cache.Remove(key);
onCacheRemove = new CacheItemRemovedCallback
(this.CacheRemoveCallback);
ctx.Cache.Add(key,ctx.Session.SessionID,null,
DateTime.Now.AddMinutes(ctx.Session.Timeout),TimeSpan.Zero,
CacheItemPriority.Normal,onCacheRemove);
}
catch(Exception exception)
{
string message = exception.Message;
// Publish Exception

}
}
The CacheRemoveCallBack method calls method PageTrackerUtil.TrackSessionEnd
to publish statistics at the end of session. The CacheCallBackMethod is synchronized to
execute at the same time when session expires.

Collapse
private void CacheRemoveCallback(string key, Object source,
CacheItemRemovedReason reason)
{
if (reason == CacheItemRemovedReason.Expired)
{
PageTrackerUtil.TrackSessionEnd(key);
}
}
• Session Module events - Use SessionStateModule to register OnSessionStart event.
Use this event to signal when someone has accessed the site.

Collapse
private void OnSessionStart(Object source, EventArgs ea)
{
PageTrackerUtil.TrackSessionStart(HttpContext.Current);
}
• PreRequestHandlerExecute - This event occurs just before ASP.NET begins executing
a page. Use this event to monitor individual pages.

Collapse
private void OnPreRequestHandlerExecute(object source, EventArgs e)
{
PageTrackerUtil.TrackSessionPages(((HttpApplication)source).Context);
}

Page Tracking Methods


• Through above HttpModule, we have hooked up three methods: TrackSessionStart,
TrackSessionPages, and TrackSessionEnd of PageTrackerUtil to take care of the
page level tracking. Since we will not have access to the Session when it is recycled by
ASP.NET, we will use our own small and simple static hash table to persist objects for
page level tracking.
• TrackSessionStart

Collapse
private static void UpdateSessionConst(string key, PageTracker tracker)
{
if (SessionConst.Session[key] == null)
{
SessionConst.Session.Add(key,tracker);
}
else
{
SessionConst.Session.Remove(key);
SessionConst.Session.Add(key,tracker);
}
}
Collapse
public static void TrackSessionStart(HttpContext ctx)
{
string key = ctx.Session.SessionID;
string siteTracking =
ConfigurationSettings.AppSettings["SiteTracking"];
string ip = ctx.Request.UserHostAddress.ToString();
if (siteTracking == null || siteTracking.Length == 0) return;
if (siteTracking.ToLower() != "none" && ValidTracking(ctx))
{
PageTracker tracker = new PageTracker(ctx);
UpdateSessionConst(key,tracker);
EADUtility.SendMail(tracker.SessionUserHostAddress,
CreateTrackerMessageBody(tracker));
}
}
At the start of the session, we instantiate PageTracker class and persist it in our static
hash table SessionConst.Session.

Collapse
using System;
using System.Collections;
namespace EAD.Constant
{
public class SessionConst
{
public static Hashtable Session = new Hashtable();
public static Hashtable Page = new Hashtable();
}
}
• TrackSessionPages

Collapse
public static void TrackSessionPages(HttpContext ctx)
{
string siteTracking =
ConfigurationSettings.AppSettings["SiteTracking"];
if (siteTracking == null || siteTracking.Length == 0 ||
siteTracking.ToLower() == "none") return;
if (ctx.Session == null) return;
string key = ctx.Session.SessionID;
siteTracking = siteTracking.ToLower();
if (siteTracking == "full" || siteTracking == "partial")
{
if (SessionConst.Session[key] != null)
{
PageTracker tracker = (PageTracker) SessionConst.Session[key];
if (tracker != null)
{
string relativeFilePath =
ctx.Request.Url.AbsolutePath.Remove(0,
ctx.Request.ApplicationPath.Length).ToLower();
// Remove things after ? mark.

int pos = relativeFilePath.IndexOf("?");


if (pos > 0)
{
relativeFilePath = relativeFilePath.Substring(0,pos);
}
if (siteTracking == "full")
{
tracker.AddPage(relativeFilePath);
}
else if (PageVisit(relativeFilePath))
{
tracker.AddPage(relativeFilePath);
}
UpdateSessionConst(key,tracker);
}
}
}
At page level tracking, we retrieve PageTracker class from the static hash table and
persist it in the same static hash table SessionConst.Session after adding the page
level information.
• TrackSessionEnd
Collapse
public static void TrackSessionEnd(string key)
{
string siteTracking =
ConfigurationSettings.AppSettings["SiteTracking"];
if (siteTracking == null || siteTracking.Length == 0) return;
if (siteTracking.ToLower() != "none")
{
if (SessionConst.Session[key] != null)
{
PageTracker tracker = (PageTracker) SessionConst.Session[key];
SessionConst.Session.Remove(key);
SessionConst.Page.Remove(key);
if (tracker != null)
{
EADUtility.SendMail(tracker.SessionUserHostAddress+"-End",
CreateTrackerMessageBody(tracker)+
CreateTrackerPageListing(tracker));
}
}
}
}

Page Tracker Class


Until now, it was only administrative procedures for executing page level tracking. The actual
page level information is stored in PageTracker class.
• The initialization of parameters is done in the constructor of PageTracker class.

Collapse
public PageTracker(HttpContext ctx)
{
string KEY = ctx.Session.SessionID;
pages = new ArrayList();
expires = DateTime.Now.AddYears(1);
sessionReferrer = (ctx.Request.UrlReferrer == null) ?
string.Empty : ctx.Request.UrlReferrer.ToString();
sessionURL = (ctx.Request.Url == null) ? string.Empty :
ctx.Request.Url.ToString();
if (SessionConst.Page[KEY] == null)
{
visitCount = 1;
originalReferrer = sessionReferrer;
originalURL = sessionURL;
SessionConst.Page.Add(KEY,new PageHashData(visitCount,
sessionReferrer,sessionURL));
}
else
{
PageHashData pageHashData = (PageHashData) SessionConst.Page[KEY];
pageHashData.PageCount++;
SessionConst.Page.Remove(KEY);
SessionConst.Page.Add(KEY,pageHashData);
visitCount = ((PageHashData)SessionConst.Page[KEY]).PageCount;
originalReferrer =
((PageHashData)SessionConst.Page[KEY]).Referrer;
originalURL = ((PageHashData)SessionConst.Page[KEY]).Url;
}
userHostAddress = ctx.Request.UserHostAddress.ToString();
userAgent = ctx.Request.UserAgent.ToString();
browser = ctx.Request.Browser.Browser;
crawler = ctx.Request.Browser.Crawler.ToString();
}
• The AddPage method is called the TrackSessionPages method.

Collapse
public void AddPage(string pageName)
{
PageData pti = new PageData();
pti.PageName = pageName;
pti.Time = DateTime.Now;
pages.Add(pti);
}
• The use of cookies to store information is also eliminated with the use of PageHashData
and PagaData classes which are stored in the Page hash table in SessionConst.

Collapse
using System;
namespace EAD.Utility
{
[Serializable]
public class PageHashData
{
private string referrer, url;
private int pageCount;
public PageHashData(int pageCount, string referrer, string url)
{
this.pageCount = pageCount;
this.referrer = referrer;
this.url = url;
}
public int PageCount { get { return pageCount;}
set{pageCount = value;} }
public string Referrer{ get { return referrer;} }
public string Url{ get { return url;} }
}
}
Collapse
using System;
namespace EAD.Utility
{
[Serializable]
public class PageData
{
public string PageName;
public DateTime Time;
}
}

Known Issues
I still have to figure out how much time a user spends on a particular page if they do not navigate
to other pages, because page statistics are only collected when the user switches to another page.
Results
The sample result of the page tracking output is similar to the output used by Wayne's article.
Sample output when someone hits to the site:
Page Tracking Information

UserHostAddre
127.0.0.1 Durham North Carolina United States
ss:

Date: 6/26/2004 3:00:05 PM

Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR


UserAgent:
1.1.4322; .NET CLR 1.0.3705)

Browser: IE

Crawler: False

http://localhost/PageTracking/index.aspx?
URL: IPCity=Durham&IPRegion=North Carolina&IPCountry=United
States

Referer:

Visits: 1

Orig Referer:

http://localhost/PageTracking/index.aspx?
Orig URL: IPCity=Durham&IPRegion=North Carolina&IPCountry=United
States

Sample output when session expires. At this time, you get the detailed statistics on how a user
has navigated your site:
Page Tracking Information

UserHostAddre
127.0.0.1 Durham North Carolina United States
ss:

Date: 6/26/2004 3:03:00 PM

Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR


UserAgent:
1.1.4322; .NET CLR 1.0.3705)

Browser: IE

Crawler: False

http://localhost/PageTracking/index.aspx?
URL: IPCity=Durham&IPRegion=North Carolina&IPCountry=United
States

Referer:

Visits: 1

Orig Referer:

Orig URL: http://localhost/PageTracking/index.aspx?


IPCity=Durham&IPRegion=North Carolina&IPCountry=United
States

Visited Pages Count: 7

Visited Pages Elapsed Time

/index.aspx 00:00:03

/index.aspx 00:00:02

/webform2.aspx 00:00:01

/webform2.aspx 00:00:00

/webform3.aspx 00:00:01

/webform3.aspx 00:00:00

/index.aspx

Total Time: 00:00:08

License
This article has no explicit license attached to it but may contain usage terms in the article text or
the download files themselves. If in doubt please contact the author via the discussion board
below.
A list of licenses authors might use can be found here
About the Author
vikramk Vikram is an Enterprise Application Architect specializing in EAI, ETL,
all relational databases and transforming legacy applications to Microsoft
.Net environment. Vikram works for a consulting firm in Research
Web Developer Triangle Park, NC. Vikram has expertise in all relational databases,
Cobol, mainframe, OO programming, C, Perl and Linux. C# is a
newfound craze for Vikram.
United States

Member
Bottom of Form

You might also like