Click here to Skip to main content
Click here to Skip to main content

Page Tracking in ASP.NET

, 23 Jun 2004
Rate this:
Please Sign up or sign in to vote.
This articles explains how to track visitors to your web site.

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.
    <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.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="MailFrom" value="<A href="mailto:yourMailFrom@yourserver.com">yourMailFrom@yourserver.com</A>" />

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:
    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.
    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.

    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.
    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.
    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
    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);
      }
    }
    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.

    using System;
    using System.Collections;
    
    namespace EAD.Constant
    {
      public class SessionConst
      {
        public static Hashtable Session = new Hashtable();
        public static Hashtable Page = new Hashtable();
      }
    }
  • TrackSessionPages
    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
    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.
    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.
    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.
    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;} }
       }
    }
    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:

<STYLE type=text/css> <!-- .Data{font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;background-color : #FFFFFF;text-align: left;} .TableLabel {font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;font-weight: bold;color: #000099;background-color: #eeeeee;text-align : left;} .Label {font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;font-weight: bold;color: #000099;background-color: #eeeeee;text-align : right;} --> </STYLE>
Page Tracking Information
UserHostAddress: 127.0.0.1 Durham North Carolina United States
Date: 6/26/2004 3:00:05 PM
UserAgent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 1.0.3705)
Browser: IE
Crawler: False
URL: http://localhost/PageTracking/index.aspx?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

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

<STYLE type=text/css> <!-- .Data{font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;background-color : #FFFFFF;text-align: left;} .TableLabel {font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;font-weight: bold;color: #000099;background-color: #eeeeee;text-align : left;} .Label {font-family : Verdana, Arial, Helvetica, sans-serif;font-size : 12px;padding : 5;font-weight: bold;color: #000099;background-color: #eeeeee;text-align : right;} --> </STYLE>
Page Tracking Information
UserHostAddress: 127.0.0.1 Durham North Carolina United States
Date: 6/26/2004 3:03:00 PM
UserAgent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 1.0.3705)
Browser: IE
Crawler: False
URL: http://localhost/PageTracking/index.aspx?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

Share

About the Author

vikramk
Web Developer
United States United States
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 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.

Comments and Discussions

 
QuestionHow we will do this in Asp.net MVC? PinmemberMember 470860826-Apr-09 23:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 24 Jun 2004
Article Copyright 2004 by vikramk
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid