Make a custom device detector in Sitecore “Poor mans device detector”

detectorDone

First I would like to thank Sitecore for the great honor of being rewarded Sitecore MVP. I am truly thankful, please visit my fellow Sitecore MVP’s all over the world – Sitecore MVPs 2016

In my recent post, Deeplinking to mobile apps using Sitecore Device Detection, I described how easy it is to detect devices using Sitecore’s awesome feature – Sitecore Device Detector. I really love it and it’s so easy to use thanks to the device rules which lets you trigger goals/events and personalize content. This awesome feature is not for free but it’s worth every penny.

This post is for you who only want to use part of the functionality and don’t want to pay for it, I was thinking of calling it “Poor mans device detector” 🙂

I got the idea when I was going through all the nice rules in Sitecore 8.1, there are so many just waiting for you to use
Rules

I noticed a rule that really could come in handy – User Agent. It allows you to look at the visitors user agent and do actions.
UserAgentRule

But it turns out that particular rule is not accessible from renderings when using personalization rules.
RenderRules
Why is that? I used my favorite tool, dotPeek, to do some peeking in the code.

using Sitecore.Rules.Conditions;
using System.Web;

namespace Sitecore.Rules.Devices
{
  /// <summary>
  /// User agent condition class.
  /// 
  /// </summary>
  /// <typeparam name="T">The rule context.</typeparam>
  public class UserAgentCondition<T> : StringOperatorCondition<T> where T : DeviceRuleContext
  {
    /// <summary>
    /// Gets or sets the value.
    /// 
    /// </summary>
    /// 
    /// <value>
    /// The value.
    /// </value>
    public string Value { get; set; }

    /// <summary>
    /// Executes the specified rule context.
    /// 
    /// </summary>
    /// <param name="ruleContext">The rule context.</param>
    /// <returns>
    /// <c>True</c>, if the condition succeeds, otherwise <c>false</c>.
    /// </returns>
    protected override bool Execute(T ruleContext)
    {
      HttpContextBase httpContext = ruleContext.HttpContext;
      if (httpContext == null || string.IsNullOrEmpty(this.Value) || (httpContext.Request == null || string.IsNullOrEmpty(httpContext.Request.UserAgent)))
        return false;
      return this.Compare(httpContext.Request.UserAgent, this.Value);
    }
  }
}

It turns out that the rule inherits DeviceRuleContext and that means it will only work when using devices:
useragentDevices

So what to do? We have the code from the rule we want(thanks to dotPeek). We just need a little change, instead of inherit from DeviceRuleContext we inherit directly from RuleContext.

namespace VisionsInCode.Foundation.SitecoreCustomizations.Rules
{

  using System.Web;
  using Sitecore.Diagnostics;
  using Sitecore.Rules;
  using Sitecore.Rules.Conditions;

  public class PoorMansDeviceDetectorCondition<T> : StringOperatorCondition<T> where T : RuleContext
  {

    public string Value { get; set; }

    /// <summary>
    /// Testable UserAgent
    /// </summary>
    public string UserAgent { get; set; }


    /// <summary>
    /// Executes the specified rule context.
    /// 
    /// </summary>
    /// <param name="ruleContext">The rule context.</param>
    /// <returns>
    /// <c>True</c>, if the condition succeeds, otherwise <c>false</c>.
    /// </returns>
    protected override bool Execute(T ruleContext)
    {
      Assert.ArgumentNotNull(ruleContext, "ruleContext");

      if (string.IsNullOrWhiteSpace(UserAgent))
        this.UserAgent = HttpContext.Current.Request.UserAgent;

      if (string.IsNullOrEmpty(this.Value) || (string.IsNullOrEmpty(this.UserAgent)))
        return false;

      return this.Compare(this.UserAgent, this.Value);
    }

  }

}

I also added the UserAgent property for testing purposes.

To test the rule we will use Sitecore FakeDB. Thanks to the great post Unit Testing Custom Rules, Actions, and Conditions with FakeDb – Part 1 – Testing Conditions by Brian Beckham. This made it easier to write the tests.

namespace VisionsInCode.Foundation.SitecoreCustomizations.Tests.Rules
{
  using FluentAssertions;
  using Sitecore.FakeDb;
  using Sitecore.Rules;
  using VisionsInCode.Foundation.SitecoreCustomizations.Rules;
  using VisionsInCode.Foundation.SitecoreCustomizations.Tests.Extensions;
  using Xunit;

  public class PoorMansDeviceDetectorConditionTests
  {
   
    private void SetupDb(Db database)
    {
      database.Add(new DbItem("Settings")
      {
        ParentID = Sitecore.ItemIDs.SystemRoot,
        Children =
        {
          new Sitecore.FakeDb.DbItem("Rules")
          {
            new Sitecore.FakeDb.DbItem("Definitions")
            {
              new Sitecore.FakeDb.DbItem("String Operators")
              {
                new Sitecore.FakeDb.DbItem(Constants.StringOperations.Contains.ItemName, Constants.StringOperations.Contains.ItemID),
                new Sitecore.FakeDb.DbItem(Constants.StringOperations.MatchesTheRegularExpression.ItemName, Constants.StringOperations.MatchesTheRegularExpression.ItemID)

              }
              
            }
          }
        }

      });
    }

    [Theory]
    [InlineAutoDbData("Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4", "iPhone", true)]
    public void DoesUserAgentContainValueCondition(string userAgent, string containsUserAgentValue, bool expectedResult, Db database)
    {


      SetupDb(database);


      RuleContext ruleContext = new RuleContext();

      PoorMansDeviceDetectorCondition<RuleContext> customUserAgentCondition = new PoorMansDeviceDetectorCondition<RuleContext>()
      {
        OperatorId = Constants.StringOperations.Contains.ItemID.ToString(),
        Value = containsUserAgentValue,
        UserAgent = userAgent
      };

      var ruleStack = new RuleStack();

      // act
      customUserAgentCondition.Evaluate(ruleContext, ruleStack);

      // assert
      ruleStack.Should().HaveCount(1);

      object value = ruleStack.Pop();

      value.Should().Be(expectedResult);

    }

    [Theory]
    [InlineAutoDbData("Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4", "^(?!.*(iPhone|iPod|iPad|Android|BlackBerry|IEMobile))", false)]
    public void DoesUserAgentMatchesTheRegularExpressionValueCondition(string userAgent, string regularExpressionValue, bool expectedResult, Db database)
    {


      SetupDb(database);


      RuleContext ruleContext = new RuleContext();

      PoorMansDeviceDetectorCondition<RuleContext> customUserAgentCondition = new PoorMansDeviceDetectorCondition<RuleContext>()
      {
        OperatorId = Constants.StringOperations.MatchesTheRegularExpression.ItemID.ToString(),
        Value = regularExpressionValue,
        UserAgent = userAgent
      };

      var ruleStack = new RuleStack();

      // act
      customUserAgentCondition.Evaluate(ruleContext, ruleStack);

      // assert
      ruleStack.Should().HaveCount(1);

      object value = ruleStack.Pop();

      value.Should().Be(expectedResult);

    }
  }
}

What is left now is to add the new rule in Sitecore, lets put it in /sitecore/system/Settings/Rules/Definitions/Elements/Device and name it Poor Mans Device Detector.
rulePoorMan

Now we have a User Agent rule when we want to personalize content for renderings.
poorMansDeviceDetectorInRenderings

I used my favorite framework – Sitecore Habitat. Feel free to check out the code on the Github – GoranHalvarsson/Habitat

That’s all for now folks 🙂


2 thoughts on “Make a custom device detector in Sitecore “Poor mans device detector”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.