what is the best way to capture page views per user in asp.net-mvc
Asked Answered
M

4

32

what is the best way to capture page views by person without slowing down performance on the site. I see that stackoverflow show page views all over the place. Are they doing an insert into a db everytime i click on a page?

In asp.net-mvc, Is there any recommended way to track page view per user (my site has a login screen) so i can review which pages people are going to and how often

Mcnabb answered 21/5, 2011 at 2:32 Comment(1)
Which version of asp.net mvc are you using?Donnie
I
19

The best way would probably be a global action filter that intercepts requests to all actions on all controllers, then increments a counter in the database for the current user and page. To save hitting the database too hard, you could cache these values and invalidate them every few minutes, depending on how much traffic you're dealing with.

Isleen answered 21/5, 2011 at 2:49 Comment(4)
can you explain in more detail how you would cache the value and invalidate them every few minutes ?Mcnabb
Check out the System.Web.Caching.Cache class - msdn.microsoft.com/en-us/library/system.web.caching.cache.aspx - it will let you cache values in memory and set an expiration date.Isleen
An action filter is not the right choice. You won't receive all requests, just MVC requests.Wretch
@Wretch don't think we're looking to track all requests here? Just page views, which could all be reasonably assumed as MVC controller actions.Isleen
D
32

First off.. if what you really care about is how are customers using my site then you most likely want to look into Google Analytics or a similar service.

But if you want a quick and dirty page view record and you are using ASP.Net MVC 3 then as Chris Fulstow mentioned you're going to want to use a mix of global action filters and caching. Here is an example.

PageViewAttribute.cs 

    public class PageViewAttribute : ActionFilterAttribute
{
    private static readonly TimeSpan pageViewDumpToDatabaseTimeSpan = new TimeSpan(0, 0, 10);

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var calledMethod = string.Format("{0} -> {1}",
                                         filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                                         filterContext.ActionDescriptor.ActionName);

        var cacheKey = string.Format("PV-{0}", calledMethod);

        var cachedResult = HttpRuntime.Cache[cacheKey];

        if(cachedResult == null)
        {
            HttpRuntime.Cache.Insert(cacheKey, new PageViewValue(), null, DateTime.Now.Add(pageViewDumpToDatabaseTimeSpan) , Cache.NoSlidingExpiration, CacheItemPriority.Default,
                                  onRemove);
        }
        else
        {
            var currentValue = (PageViewValue) cachedResult;

            currentValue.Value++;
        }
    }

    private static void onRemove(string key, object value, CacheItemRemovedReason reason)
    {
        if (!key.StartsWith("PV-"))
        {
            return;
        }

        // write out the value to the database
    }

    // Used to get around weird cache behavior with value types
    public class PageViewValue
    {
        public PageViewValue()
        {
            Value = 1;
        }

        public int Value { get; set; }
    }
}

And in your Global.asax.cs

public class MvcApplication : HttpApplication 
{
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new PageViewAttribute());
        }
}

For pre-ASP.Net MVC 3 ONLY you are going to have to apply the same attribute manually to all of your actions.

[PageView]
public ActionResult CallOne()
{
}

[PageView]
public ActionResult CallTwo()
{
}
Donnie answered 16/6, 2011 at 17:35 Comment(11)
@ShaneC - thanks for your response . .the only issue is that i don't see you persisting this anywhereMcnabb
No clue how your database is setup.. that part my friend is up to you :) see in onRemove where it says // write out the value to the databaseDonnie
@ShaneC - when is OnRemove Called ? On ever single page load?Mcnabb
No it's only called when the cache expires. It's being passed into the Cache.Insert call as the On remove callback.Donnie
Also the timeout I specified is 10 seconds.. you may want longer.. I was just using that from a test perspective.Donnie
@ShaneC - ah thanks .. not sure how i missed that. so would you suggest running an update statement at that point? i am trying to think of scenarios where the "currentValue" variable would get out of sync with the database. What if i restarted the server . wouldn't that reset the "currentValue" variable to 0 . . i guess if your cachedResult == null then i guess it would make sense to read the value from the db at that point . . am i missing something ?Mcnabb
Yeah you would want to update the database. And if you restart the server you could in theory write something in your Application_End() to handle that scenario IF you cared about the last N minutes of data. Personally I doubt it would be worth the effort.Donnie
It should be noted that this solution simply delays the database hit. However, it does not batch the DB hits for efficiency. Hence, your database will still be hit the same number of times, which for a busy/popular website makes no real difference.Hollyhock
An action filter is not the right choice. You won't receive all requests, just MVC requests.Wretch
@Wretch - you don't care about the rest (e.g content requests)Dismuke
This works well, but it also counts things like child actions, getting JSON content over Ajax, redirects... you may want to use OnActionExecuted instead and check that filterContext.Result is ViewResult, for more accuracy.Emmittemmons
I
19

The best way would probably be a global action filter that intercepts requests to all actions on all controllers, then increments a counter in the database for the current user and page. To save hitting the database too hard, you could cache these values and invalidate them every few minutes, depending on how much traffic you're dealing with.

Isleen answered 21/5, 2011 at 2:49 Comment(4)
can you explain in more detail how you would cache the value and invalidate them every few minutes ?Mcnabb
Check out the System.Web.Caching.Cache class - msdn.microsoft.com/en-us/library/system.web.caching.cache.aspx - it will let you cache values in memory and set an expiration date.Isleen
An action filter is not the right choice. You won't receive all requests, just MVC requests.Wretch
@Wretch don't think we're looking to track all requests here? Just page views, which could all be reasonably assumed as MVC controller actions.Isleen
T
5

We use the open source Piwik: http://piwik.org/, which is setup on it's own server. One line of Javascript in the _Layout page makes a call to Piwik after the page has loaded (put the JS at the end) and does not affect page load performance at all.

In addition to just counts, you'll get a ton of info about where your users are coming from, browser, screen resolutions, installed plugins. Plus you can track conversions and use the same tool to track marketing campaigns, etc.

<soapbox>

I cannot think of a situation where you'd be better off implementing this in MVC or in your web app in general. This stuff simply does not belong in your web app and is a meta-concern that should be separated out. This approach has enabled us to track analytics for all of our apps (32 of them: mvc 2/3, webforms, php...) in a unified manner.

If you really don't want to use another tool for this purpose, I would recommend tapping into your IIS log and getting your stats from there. Again, to get any real decision making power out of it, you'll need to put a good analyzer on it. I recommend Splunk: http://www.splunk.com/

</soapbox>

Truce answered 17/6, 2011 at 18:12 Comment(3)
There is an argument for putting the JavaScript for this type of analysis nearer the top of your page as it will be able to log abandoned page requests or very fast page requests - for example if I click on "Products" and before the page has finished loading I spot a link for "Cameras" and click on it - the script at the bottom of the page wouldn't actually log my visit to the "Products" page.Carib
@Sohnee: Agree. Piwik also has a way to track hits by embedding a 1 pixel image. If Javascript performance becomes a concern this would be a somewhat less powerful alternative.Truce
@xameeramir Yes, but our web team has found Google Analytics to be better so we're in the process of switching. Plus we're not fans of PHP.Truce
F
1

I wanted to post an updated version of Shane's answer for those who are interested. Some things to consider:

  1. You have to set the action attribute up as a service when decorating your methods using syntax like the following :

    [ServiceFilter(typeof(PageViewAttribute))]

  2. As far as I can tell, HttpRuntime.Cache.Insert isn't a thing in .NET Core, so I used a simple implementation of IMemoryCache (You may need to add this line to your startup.cs in order to use the interface):

    services.AddMemoryCache();

  3. Because we are injecting IMemoryCache into a class that is not a controller, we need to register our attribute as a service in startup.cs, like so:

    services.AddScoped<[PageViewAttribute]>(); - without brackets!

  4. Whatever object you return when creating a cacheKey will be assigned to the 'value' parameter of the OnRemove method.

Below is the code.

public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var controllerActionDescriptor = filterContext.ActionDescriptor as ControllerActionDescriptor;

        var arguments = filterContext.ActionArguments;
        ActionId = arguments["id"].ToString();

        var calledMethod = string.Format("{0} -> {1}",
                                         controllerActionDescriptor.ControllerName,
                                         controllerActionDescriptor.ActionName);

        var cacheKey = string.Format("PV-{0}", calledMethod);

        var cachedResult = _memoryCache.Get(cacheKey);

        if (cachedResult == null)
        {
            //Get cacheKey if found, if not create cache key with following settings
            _memoryCache.GetOrCreate(cacheKey, cacheKey =>
            {
                cacheKey.AbsoluteExpirationRelativeToNow
                              = pageViewDumpToDatabaseTimeSpan;
                cacheKey.SetValue(1);
                cacheKey.RegisterPostEvictionCallback(onRemove);
                return cacheKey.Value;
            });
        }
        else
        {
            _memoryCache.Get(cacheKey);
        }
    }

    //Called when Memory entry is removed
    private void onRemove(object key, object value, EvictionReason reason, object state)
    {
        if (!key.ToString().StartsWith("PV-"))
        {
            return;
        }

        // write out the value to the database
        SaveToDataBase(key.ToString(), (int)value);

    }

As a point of reference, this was done for a .NET Core 5 MVC App.

Regards.

Formica answered 24/10, 2021 at 21:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.