Sitecore and Maxmind

I have been working on a project recently that needed to display content depending on where the site visitor came from. Content within the site was split into geographical regions. This sounds perfect for the Location Api within Sitecore, but Sitecore have modified how they determine your country from your IP Address from a sync call to an async call, and because of this you cannot rely on the Sitecore location services.

My client needed to make sure that content was set to the correct region from first visit so I had to come up with an alternative solution.

As the location api is using Maxmind, and reviewing what is on offer from their website there is a database you can download. All that is needed is to store GeoLite2-Country.mmdb in the App_Data folder.

Maxmind also has a Nuget package to use with their database, MaxMind.DB. However you will have to use version 1 of the package because later versions require mvc 5.2, and Sitecore 8.0 is currently using mvc 5.1. At my current employer we do not use any version of the .net framework that is later than the version used by Sitecore.

Therefore you need to use nuget package MaxMind.Db.1.0.0.0 only. Use later version at your own risk.

So the first thing need to do is get the Ip Address. I am retrieving the Ip Address from the HttpContext, but within Sitecore there are other ways of getting the ip address.

First thing to start with is to do a wrapper around HttpContext.
So start with an interface that we can use.

public interface IHttpContext
{
    string IpAddress();
}

Next need the class to retrieve the IpAddress. However there is one thing to remember, on your local development environment you are probably going to get a private ip address, and using the MaxMind DB you are not going to get any location information.
The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets:

0.0.0.0 – 10.255.255.255 (10/8 prefix)
172.16.0.0 – 172.31.255.255 (172.16/12 prefix)
192.168.0.0 – 192.168.255.255 (192.168/16 prefix)

To take this into account, I have created a private method to check to see if a private ip address is returned, and if is, then return a public ip address instead.

public class HttpContextWrapper : IHttpContext
{
    public string IpAddress()
    {
        var ipaddress = HttpContext.Current.Request.UserHostAddress;
        const string publicIpAddress = "178.78.112.129";
        return LocalOrPrivateIpAddress(ipaddress) 
            ? publicIpAddress : ipaddress;
        // For dev purposes only so dont get 127.0.0.1 or other 
        // private ip address range 10.x.x.x, 172.x.x.x, 192.x.x.x
    }

    private bool LocalOrPrivateIpAddress(string ipAddress)
    {
        var sections = ipAddress.Split('.');
        var localIpAddress = new[]{"127", "10", "172", "192"};
        if (!localIpAddress.Contains(sections[0]))
        {
            return false;
        }

        if (sections[0].Equals("127") || sections[0].Equals("10"))
        {
            return true;
        }

        if (sections[0].Equals("172"))
        {
            return Valid172PrivateAddress(sections);
        }
        return Valid192PrivateAddress(sections);
    }

    private bool Valid172PrivateAddress(string[] sections)
    {
        var range = int.Parse(sections[1]);
        return (range > 15 || range < 32);
    }

    private bool Valid192PrivateAddress(string[] sections)
    {
        return (sections[2].Equals(&quot;168&quot;));
    }
}

Now that we can get a valid public ip address, we need to use the MaxMind db to get the country information. Reviewing the documentation, you need the following classes

public class CountryResponse
{
    public Continent continent { get; set; }
    public Country country { get; set; }
    public RegisteredCountry registered_country { get; set; }
}

public class Continent
{
    public string code { get; set; }
    public int geoname_id { get; set; }
    public LanguageNames names { get; set; }
}

public class Country
{
    public int geoname_id { get; set; }
    public string iso_code { get; set; }
    public LanguageNames names { get; set; }
}

public class RegisteredCountry
{
    public int geoname_id { get; set; }
    public string iso_code { get; set; }
    public LanguageNames names { get; set; }
}

public class LanguageNames
{
    public string en { get; set; }
    public string ru { get; set; }
    public string es { get; set; }
    [JsonProperty("zh-CN")]
    public string zh { get; set; }
    public string fr { get; set; }
    public string ja { get; set; }
    [JsonProperty("pt-BR")]
    public string pt { get; set; }
    public string de { get; set; }
}

First start with an interface that will return the CountryResponse from a search

public interface IIpSearch
{
    CountryResponse SearchFor(string ip);
}

And the actual class

public class IpSearch : IIpSearch 
{
    private readonly ISettings _settings;
        
    public IpSearch(ISettings settings)
    {
        _settings = settings;
    }

    public CountryResponse SearchFor(string ip)
    {
        var dbLocation = HostingEnvironment.
            MapPath(_settings.MaxMindmmdbDatabaseLocation);
        using (var reader = new Reader(dbLocation))
        {
            JToken token = reader.Find(ip);
            JObject ipObject = (JObject)token;
            if (ipObject == null)
            {
                return null;
            }
            var response = ipObject.ToObject();
            return response;
        }
    }
}

Within Sitecore we then created all the regions that the customer wanted, and all the counties. Each country was set to a region, so that once determined the country from the Ip address, we could display the correct region for the site visitor.

All that is required is a method to put it all together

public RegionItemViewModel GetRegionFromIp()
{
    var ip = _httpContext.IpAddress();
    var geoIpCountryInformation = _ipSearch.SearchFor(ip);
     if (geoIpCountryInformation == null)
    {
        return null;
    }
    var region = _countrySearch.SearchFor(
        geoIpCountryInformation.country.iso_code);
    return region;
}

_countrySearch is a class that does a search within Sitecore to return the country and region information.

Now that we have got our country and region, the last thing to do is store this information within analytics so that it can be used in any way desired.

What is needed is the interface to set the analytics information

public interface IAnalyticsTracker
{
    bool IsActive();
    void SetSitecoreVisitorTag(string tagName, string tagValue);
}

Add the class to set the information

using Sitecore.Analytics;
using Sitecore.Analytics.Tracking;

public class AnalyticsTracker : IAnalyticsTracker
{
    public AnalyticsTracker(ILogger logger, ISettings settings)
    {
    }

    public bool IsActive()
    {
        return Tracker.IsActive;
    }

    public void SetSitecoreVisitorTag(string tagName, 
        string tagValue)
    {
        if (!(IsActive()))
        {
            return;
        }
        var visitor = Tracker.Current.Contact;
        if (visitor == null)
            return;

        SetTag(visitor, tagName, tagValue);
        SetCustomData(tagName, tagValue);
    }

    private static void SetCustomData(string tagName, string tagValue)
    {
        if (!Tracker.Current.Session.CustomData.ContainsKey(tagName))
        {
            Tracker.Current.Session.CustomData.Add(tagName, tagValue);
        }
        else
        {
            if (!Tracker.Current.Session.CustomData[tagName].Equals(tagValue))
            {
                Tracker.Current.Session.CustomData[tagName] = tagValue;
            }
        }
    }

    private static void SetTag(Contact visitor, string tagName, string tagValue)
    {
        var visitorTags = visitor.Tags.Find(tagName);
        if (visitorTags == null)
        {
            visitor.Tags.Add(tagName, tagValue);
        }
        else
        {
            if (!visitor.Tags[tagName].Equals(tagValue))
            {
                visitor.Tags[tagName] = tagValue;
            }
        }
    }
}

I am not happy with the if then else I have written, but going to leave for now.

All that is needed is to set the information

public void SetRegion(RegionItemViewModel newRegion)
{
    if (newRegion == null)
    {
        return;
    }
    _analyticsTracker.SetSitecoreVisitorTag(
        _settings.AnalyticsCountryName, newRegion.CountryName);
    _analyticsTracker.SetSitecoreVisitorTag(
        _settings.AnalyticsRegionName, newRegion.RegionName);
}

What not shown is the required code to ensure that only call the MaxMind DB once per call, and I have omitted the standard Sitecore API calls to search the content tree.

Advertisements

My musing about anything and everything

Tagged with: , ,
Posted in MaxMind, Sitecore

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 )

Google+ photo

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

Connecting to %s

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 12 other followers

%d bloggers like this: