Monthly Archives: June 2014

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 🙂

Advertisements

Sharing data? That would be cool

Awesome

I would like to give all Sitecore MVPs out there a big hand. Thanks for all the great posts about the new cool stuff from Sitecore. I feel like Emmet in the Lego Movie singing “Everything Is Awesome”.

I recently watched a great video from Milwaukee Sitecore Developer Meetup,
where Nick Wesselman did a great presentaion of Sitecore 7.5 and the Experience Database.

There was one thing that really caught my eye, the idea of sharing session between devices. That means if you are browsing a website from your computer, you can continue browsing from your mobile phone and the session will not be lost.
This is über cool! Sitecore will also be offering services where you can store the experience data(visitor data) in the cloud.

When watching this it suddenly struck me, why not go a step further. In order to give the best experience for the visitor you should know the user.

What if websites could share data?

Sitecore could have a service in the Cloud – Experience Broker Hub 🙂
Where all websites using Sitecore could store/sell and retrieve/buy user data.
Now the websites can give the visitors the best experience and make some money on the user data.

I know it’s crazy but it would be cool.

That’s all for now folks 🙂