Geo push emails with real-time Geodata using Sitecore Automation Engagement Plan

GeStart1

A lot of users are using devices with GPS for browsing the internet. That means we can get their actual location thanks to the HTML5 Geolocation API.

After my previous post, Geofencing with real-time Geodata in Sitecore DMS, I wanted to test the idea of geo-fencing using the Engagement Automation Plan in Sitecore.

How about pushing emails whenever a user is crossing a geo-fence ?
When the user is browsing the website an email will be sent if the user is near a geo target. In this case the geo targets are Sitecore partners.

I think the Engagement Automation Plan in Sitecore is great and you can do a lot of cool things. I love the way the “flows” are graphically presented. I’m still a novice when it comes to working with the Engagement Automation Plan but as Mike Reynolds (sitecorejunkie.com) said,

Want to learn Sitecore? Start blogging

That is exactly what I’m doing 🙂

The cool thing about the Engagement Automation Plan is that you can use rules. Each Action will contain rules that will check if a user is close to a geo location and has a valid email. If so, send an email.
EngagementPlan

Each Action will have following rules
actionRules

With the goals we can make some nice graphs for the Executive Dashboard.
ExecutiveDashboardVisitorsTip: If you don’t see any data in the graphs, change the attribute MinimumVisitsFilter from 50 to 1 in config file sitecore\shell\Applications\Reports\Dashboard\Configuration.config

Let us begin 🙂

Normally when you open the “Set rule editor” from an Action in the Engagement Plan you will not see actions, only conditions.
RuleSetEditorNoActions
That is confusing because the text on the top says – Select the conditions and actions first. I understand why they are not visible; the idea is to use the specific “Engagement Automation Plan” actions – AutomationAction. But in this case I want to use the actions within the Rule editor. They are clean and easy to work with.

How do I make the actions visible? It was not easy to find out, and it took a lot of searching before I found this great post from Adam Conn, Rules Field Type and Sitecore 6.5. He explains that the rule editor is configured with the following parameters using query string notation:
rulespath – path to the conditions and actions to display
hideactions – true if you want to hide actions, false if you want to show actions
So I changed hideactions to false in the template item /sitecore/templates/System/Analytics/Engagement Automation/Engagement Plan Condition.
RulesPathRuleEditor
Finally I can work with some cool Actions 🙂

The rule “In range of a target location” will check if a user is close to a geo target and trigger a goal.(The condition rule and the geo targets are described in my previous post Geofencing with real-time Geodata in Sitecore DMS).
RuleEditor

The goals are created in the “Marketing Center”.
VisitGoal

The action that triggers the goal defined in Sitecore
TriggerGoal

The code for the action.

public class TriggerGoal<T> : RuleAction<T> where T : RuleContext
{
    public string GoalId { get; set; }

    public override void Apply([NotNull] T ruleContext)
    {
        Assert.ArgumentNotNull((object)ruleContext, "ruleContext");

        Visitor visitor = Tracker.Visitor;

        if (visitor == null)
            return;

        if (string.IsNullOrWhiteSpace(GoalId))
            return ;

        VisitorLoadOptions visitorLoadOptions = new VisitorLoadOptions()
        {
            Options = VisitorOptions.VisitorTags,
        };

        visitor.Load(visitorLoadOptions);

        if (visitor.Tags == null)
            return;
        
        VisitorDataSet.VisitorTagsRow pageUrlTagsRow = visitor.Tags.Find(InputDataKeys.PageUrl.ToString());

        if (pageUrlTagsRow == null)
            return;

        if (string.IsNullOrWhiteSpace(pageUrlTagsRow.TagValue))
            return;

        TrackerService.RegisterEventToAPage(GoalId, pageUrlTagsRow.TagValue);

        Tracker.Submit();
    }
}

The current page URL is stored in a visitor tag, which means that I can register the goal on the correct page. Tracker.Submit() will save the event to the Analytics database.

The rule “Send a geo push” will again check if a user is close to a geo target but will also check if the user has a valid email. If so, a new goal will be triggered.
Rule2

The goals are created in the “Marketing Center”.
GeoPushGoal

The condition rule that validates the users email address defined in Sitecore
VisitorHasValidEmailCondition

The code for the condition rule.

public class VisitorIsEmailValidInEmailTag<T> : WhenCondition<T> where T : RuleContext
{
    protected override bool Execute(T ruleContext)
    {

        try
        {
            Assert.ArgumentNotNull((object)ruleContext, "ruleContext");

            Visitor visitor = Tracker.Visitor;

            if (visitor == null)
                return false;

            VisitorLoadOptions visitorLoadOptions = new VisitorLoadOptions()
            {
                Options = VisitorOptions.VisitorTags
            };

            visitor.Load(visitorLoadOptions);

            if (visitor.Tags == null)
                return false;

            string email = visitor.Tags["Email"];

            return IsEmailValidService.IsValid(email);

        }
        catch (Exception ex)
        {
            Sitecore.Diagnostics.Log.Error("Error occurred when validating email", this);
            return false;
        }
    }

}

If both rules (“In range of a target location” and “Send a geo push”) are true they will set off an AutomationAction which will take the email address from the visitor’s tag and send an email.
EmailAction

SendEmailUsingEmailTagFromVisitor(AutomationAction) defined in Sitecore.
If you are working with AutomationActions don’t forget to put them under /sitecore/system/Settings/Analytics/Engagement Automation
EmailActionSitecore

The code for SendEmailUsingEmailTagFromVisitor. I copied Sitecore.Automation.MarketingAutomation.AutomationActions.SendEmailMessageAction and altered method GetVisitorEmail.

public class SendEmailUsingEmailTagFromVisitor : AutomationAction
{
    private string BaseUrl { get; set; }

    private string From { get; set; }

    private string Host { get; set; }

    private string Login { get; set; }

    private string Mail { get; set; }

    private string Password { get; set; }

    private int Port { get; set; }

    private string Subject { get; set; }

    private string To { get; set; }

    private bool IsReadyToSend
    {
        get
        {
            if (!string.IsNullOrEmpty(this.From) && !string.IsNullOrEmpty(this.To))
                return !string.IsNullOrEmpty(this.Subject);
            else
                return false;
        }
    }

    public override AutomationActionResult Execute(VisitorDataSet.AutomationStatesRow automationStatesRow, Item action, bool isBackgroundThread)
    {
        Assert.ArgumentNotNull((object)automationStatesRow, "automationStatesRow");
        Assert.ArgumentNotNull((object)action, "action");
        this.InitMailServerSettings();
        this.InitMessageSettings(automationStatesRow, this.GetParameters(action));
        if (this.IsReadyToSend)
        {
            try
            {
                this.SendMail();
            }
            catch (Exception ex)
            {
                Log.Error(ex.Message, ex, this.GetType());
            }
        }
        return AutomationActionResult.Continue;
    }

    private string GetVisitorEmail(VisitorDataSet.AutomationStatesRow automationStatesRow)
    {
        Visitor visitor = this.GetAutomationVisitor(automationStatesRow);

        VisitorLoadOptions visitorLoadOptions = new VisitorLoadOptions()
        {
            Options = VisitorOptions.VisitorTags
        };

        visitor.Load(visitorLoadOptions);

        return visitor.Tags["Email"];
    }

    private void InitMailServerSettings()
    {
        this.Host = Settings.MailServer;
        this.Login = Settings.MailServerUserName;
        this.Password = Settings.MailServerPassword;
        this.Port = Settings.MailServerPort;
    }

    private void InitMessageSettings(VisitorDataSet.AutomationStatesRow automationStatesRow, NameValueCollection parameters)
    {
        this.From = string.IsNullOrEmpty(parameters[SendEmailMessageAction.Parameters.FromName]) ? parameters[SendEmailMessageAction.Parameters.FromEmail] : string.Format("\"{0}\" <{1}>", (object)parameters[SendEmailMessageAction.Parameters.FromName], (object)parameters[SendEmailMessageAction.Parameters.FromEmail]);
        this.Subject = parameters[SendEmailMessageAction.Parameters.Subject];
        this.Mail = parameters[SendEmailMessageAction.Parameters.Content];
        this.BaseUrl = parameters[SendEmailMessageAction.Parameters.BaseSiteUrl];
        this.To = string.IsNullOrEmpty(parameters[SendEmailMessageAction.Parameters.FixedEmail]) ? this.GetVisitorEmail(automationStatesRow) : parameters[SendEmailMessageAction.Parameters.FixedEmail];
        if (!string.IsNullOrEmpty(this.To))
            return;
        Log.Error("Destination email address (To:) not found. Send Email will not be executed.", this.GetType());
    }

    private void SendMail()
    {
        ProcessEmailMessageArgs emailMessageArgs = new ProcessEmailMessageArgs()
        {
            IsBodyHtml = true
        };
        emailMessageArgs.BaseUrl = this.BaseUrl;
        emailMessageArgs.To.Append(this.To.Replace(";", ","));
        emailMessageArgs.From = this.From;
        emailMessageArgs.Mail.Append(this.Mail);
        emailMessageArgs.Subject.Append(this.Subject);
        emailMessageArgs.Host = this.Host;
        emailMessageArgs.Port = this.Port;
        if (!string.IsNullOrEmpty(this.Login))
            emailMessageArgs.Credentials = (ICredentialsByHost)new NetworkCredential(this.Login.Replace("\\", "\\\\"), this.Password);
        CorePipeline.Run("processEmailMessage", (PipelineArgs)emailMessageArgs);
    }

    public static class Parameters
    {
        public static readonly string BaseSiteUrl = "BaseSiteURL";
        public static readonly string FromName = "FromName";
        public static readonly string FromEmail = "FromEmail";
        public static readonly string Subject = "Subject";
        public static readonly string Content = "Content";
        public static readonly string FixedEmail = "FixedEmail";

        static Parameters()
        {
        }
    }
}

Now we can create an email and get the receiver’s email address from the visitor tag
GeoPushMail

To make it all work we need to do some stuff on the client side. We have to get the user’s current location, the current page, the engagement plan state we want to enroll the user in, and the email address. (Normally the visitor is already identified and mapped to a user…)

<div id="justAdiv"
 data-engagementplan_state="<%# Sitecore.Context.Database.GetItem(Constants.Items.ClientTrackerSettings).GetMultiListValues(Constants.Fields.ClientTrackerSettings.ClientTrackerSelectedState).Select(item=>item.ID).FirstOrDefault() %>"
 data-pageurl="<%= HttpContext.Current.Request.Url.PathAndQuery %>"
 data-email="goranhalvarsson@gmail.com">
</div> 

In the module Client Tracker the engagemant plan state is selected
GeoTrackerSettings

The javascript…

var SharedSource = SharedSource || {};


jQuery(document).ready(function () {
    SharedSource.ClientTracker.DomReady();
});

SharedSource.ClientTracker = {
    DomReady: function () {

        SharedSource.ClientTracker.Init(SharedSource.ClientTracker.TrackTypes.TrackPerRequest);

    },

    Init: function (trackType) {

        window.pathToHandler = '/components/sharedsource/TrackerHandler.ashx';

        if (!navigator || jQuery("body").data("isinpageeditor").toLowerCase() == "true")
            return;

        if (trackType == SharedSource.ClientTracker.TrackTypes.TrackPerRequest)
            navigator.geolocation.getCurrentPosition(geoSuccess, geoError);

        if (trackType == SharedSource.ClientTracker.TrackTypes.FrequentTracking) {
            // see https://developer.mozilla.org/en-US/docs/Web/API/Geolocation.watchPosition
            var watchID = navigator.geolocation.watchPosition(geoSuccess, geoError, SharedSource.ClientTracker.TrackFrequenzyOptions);
        }

        
        function geoSuccess(p) {
            window.coordinates = p.coords;
            SharedSource.ClientTracker.TrackUserLocation(jQuery("#justAdiv"));
        }

        function geoError(error) {
            var message = "";
            switch (error.code) {
                case error.PERMISSION_DENIED:
                    message = "This website does not have permission to use " + "the Geolocation API";
                    break;
                case error.POSITION_UNAVAILABLE:
                    message = "The current position could not be determined.";
                    break;
                case error.PERMISSION_DENIED_TIMEOUT:
                    message = "The current position could not be determined " + "within the specified timeout period.";
                    break;
            }

            if (message == "") {
                var strErrorCode = error.code.toString();
                message = "The position could not be determined due to " + "an unknown error (Code: " + strErrorCode + ").";
            }

            SharedSource.ClientTracker.Logging(message);
        };


    },
    TrackUserLocation: function (dataContainer) {

        if (!window.coordinates)
            return;

        var requestParamAndValues = {};
        requestParamAndValues["Coordinates"] = SharedSource.ClientTracker.StringFormat("{0},{1}", window.coordinates.latitude, window.coordinates.longitude, '1');

        requestParamAndValues["GeoSpeed"] = window.coordinates.speed;

        if (window.coordinates.heading != null)
            requestParamAndValues["GeoHeading"] = window.coordinates.heading;

        if (dataContainer.data("engagementplan_state"))
            requestParamAndValues["EngagementPlanState"] = dataContainer.data("engagementplan_state");
        
        if (dataContainer.data("pageurl"))
            requestParamAndValues["PageUrl"] = dataContainer.data("pageurl");
        
        if (dataContainer.data("email"))
            requestParamAndValues["Email"] = dataContainer.data("email");

        var jsonObject = {};
        jsonObject["requestParamAndValues"] = requestParamAndValues;

        var analyticsEvent = new AnalyticsPageEvent(jsonObject, window.pathToHandler);
        analyticsEvent.trigger();

    },
    StringFormat: function () {
        var s = arguments[0];
        for (var i = 0; i < arguments.length - 1; i++) {
            var reg = new RegExp("\\{" + i + "\\}", "gm");
            s = s.replace(reg, arguments[i + 1]);
        }
        return s;
    },

    Logging: function (message) {
        if (typeof console == "object") {
            console.log(message);
        }
    },

    TrackTypes: {
        "NoTracking": 1,
        "TrackPerRequest": 2,
        "FrequentTracking": 3
    },

    TrackFrequenzyOptions: { //  see https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions
        enableHighAccuracy: true,
        timeout: 30000, // Every 10 second 
        maximumAge: 0 //No caching
    }

};

Next thing to do is to store the data. I’m very fond off the Visitor Tags in Sitecore DMS so that is where I will put it. I will use the TrackerHandler.ashx from previous post, Client Tracker with Sitecore DMS.

public class TrackerHandler : IHttpHandler, IRequiresSessionState
{

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = Constants.ResponseContentTypes.ApplicationJavascript;
        Execute(context);
    }

    private static void Execute(HttpContext context)
    {

        if (!AnalyticsSettings.Enabled)
            return;

        string jsonData = context.Request[Constants.QueryParameters.JsonData];

        if (string.IsNullOrWhiteSpace(jsonData))
        {
            context.Request.InputStream.Position = 0;
            using (var inputStream = new StreamReader(context.Request.InputStream))
            {
                jsonData = inputStream.ReadToEnd();
            }
        }

        InputData inputData = JsonConvert.DeserializeObject<InputData>(jsonData);

        if (inputData == null)
            return;

        if (!Tracker.IsActive)
            Tracker.StartTracking();

        Tracker.CurrentPage.Cancel();

        if (inputData.ContainsParamkey(InputDataKeys.Coordinates))
        {
            TrackerService.SetCurrentVisitorCoordinates(inputData.GetValueByKey(InputDataKeys.Coordinates));
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.Coordinates.ToString(), inputData.GetValueByKey(InputDataKeys.Coordinates));
        }


        if (inputData.ContainsParamkey(InputDataKeys.Email))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.Email.ToString(), inputData.GetValueByKey(InputDataKeys.Email));
        
        if (inputData.ContainsParamkey(InputDataKeys.GeoSpeed))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.GeoSpeed.ToString(), inputData.GetValueByKey(InputDataKeys.GeoSpeed));

        if (inputData.ContainsParamkey(InputDataKeys.GeoHeading))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.GeoHeading.ToString(), inputData.GetValueByKey(InputDataKeys.GeoHeading));

        if (inputData.ContainsParamkey(InputDataKeys.PageUrl) && inputData.ContainsParamkey(InputDataKeys.PageEventId))
            TrackerService.RegisterEventToAPage(inputData.GetValueByKey(InputDataKeys.PageEventId), inputData.GetValueByKey(InputDataKeys.PageUrl));

        if (!string.IsNullOrWhiteSpace(inputData.GetValueByKey(InputDataKeys.EngagementPlanState)))
            TrackerService.EnrollCurrentVisitorInEngagementPlanState(new ID(inputData.GetValueByKey(InputDataKeys.EngagementPlanState)));
            
        if (inputData.ContainsParamkey(InputDataKeys.PageUrl)) 
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.PageUrl.ToString(), inputData.GetValueByKey(InputDataKeys.PageUrl));

        Tracker.Submit();

         
    }


    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

}

In the class TrackerService, I’ve added the method EnrollCurrentVisitorInEngagementPlanState. I found out that you don’t need the ExternalUser to enroll the visitor in a specific state of an engagement plan.

public static void EnrollCurrentVisitorInEngagementPlanState(ID stateId)
{
    Visitor visitor = Tracker.Visitor;

    VisitorLoadOptions visitorLoadOption = new VisitorLoadOptions
    {
        Options = VisitorOptions.AutomationStates
    };

    visitor.Load(visitorLoadOption);

    VisitorManager.AddVisitors(new List<Guid> { Tracker.CurrentVisit.VisitorId }, stateId);
}

That’s all for now folks 🙂


2 thoughts on “Geo push emails with real-time Geodata using Sitecore Automation Engagement Plan

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.