This blog demonstrates how to add or update Sitecore contact using FXM JavaScript API. Federated Experience Manager (FXM) application allows you to add Sitecore content on external non-Sitecore websites and track visitor interactions, generate analytics information as if it was a Sitecore site. It also allows us to leverage Sitecore personalization based on contact Identified. However, we had the following question – How can we identify the contact from the Non-Sitecore site using FXM, and then use the Sitecore Personalization engine to deliver targeted content?. This is what we came up with.

Sitecore FXM uses what they call a Beacon script to track and record the current users’ behavior (i.e interactions). Including this script will allow you to execute the following methods to track events, goals, outcomes, and campaigns. However, there are no available methods to identify a contact or update a contact’s facet data. This functionality is needed when we have a Non-Sitecore site using FXM that is also the application responsible for logging the user in, and then personalizing content based on a given attribute stored in xDb.

  1. SCBeacon.trackEvent
  2. SCBeacon.trackGoal
  3. SCBeacon.trackOutcome
  4. SCBeacon.trackCampaign

Looking at Sitecore 9.1 configs for FXM in Sitecore.FXM.config and Sitecore FXM assembly lead us to the TriggerEventsProcessor which is responsible for handling calls the Beacon method’s referrenced above.

<tracking.triggerpageevent>

  <processor type="Sitecore.FXM.Pipelines.Tracking.TriggerPageEvent.TriggerEventsProcessor, Sitecore.FXM" resolve="true" />

</tracking.triggerpageevent>
public class TriggerEventsProcessor : ITriggerPageEventProcessor
  {
    private readonly IAnalyticsDefinitionsRepository _analyticsDefinitionsRepository;
    public TriggerEventsProcessor(IAnalyticsDefinitionsRepository analyticsDefinitionsRepository)
    {
      Assert.ArgumentNotNull((object) analyticsDefinitionsRepository, nameof (analyticsDefinitionsRepository));
      this._analyticsDefinitionsRepository = analyticsDefinitionsRepository;
    }
    public void Process(TriggerPageEventArgs args)
    {
      IPageContext currentPageContext = args.CurrentPageContext;
      Assert.IsNotNull((object) currentPageContext, "The current page is not tracked in the current session. No events can be triggered.");
      ITrackDescriptor trackDescriptor = args.TrackDescriptor;
      if (trackDescriptor is EventTrackDescriptor)
      {
        EventTrackDescriptor eventTrackDescriptor = (EventTrackDescriptor) trackDescriptor;
        this.TriggerPageEvent((IDefinition) eventTrackDescriptor.Definition, currentPageContext, eventTrackDescriptor.Data, eventTrackDescriptor.DataKey, eventTrackDescriptor.Extras);
      }
      else if (trackDescriptor is GoalTrackDescriptor)
      {
        GoalTrackDescriptor goalTrackDescriptor = (GoalTrackDescriptor) trackDescriptor;
        this.TriggerPageEvent((IDefinition) goalTrackDescriptor.Definition, currentPageContext, goalTrackDescriptor.Data, goalTrackDescriptor.DataKey, goalTrackDescriptor.Extras);
      }
      else if (trackDescriptor is CampaignTrackDescriptor)
      {
        CampaignTrackDescriptor campaignTrackDescriptor = (CampaignTrackDescriptor) trackDescriptor;
        currentPageContext.TriggerCampaign(campaignTrackDescriptor.Definition);
      }
      else if (trackDescriptor is OutcomeTrackDescriptor)
      {
        OutcomeTrackDescriptor outcomeTrackDescriptor = (OutcomeTrackDescriptor) trackDescriptor;
        OutcomeData outcome = new OutcomeData(outcomeTrackDescriptor.Definition, outcomeTrackDescriptor.CurrencyCode, outcomeTrackDescriptor.Monetary);
        IDictionary<string, string> extras = outcomeTrackDescriptor.Extras;
        if (extras != null && extras.Count > 0)
          outcome.CustomValues["FXM_CustomValues"] = (object) outcomeTrackDescriptor.Extras;
        currentPageContext.RegisterOutcome(outcome);
      }
      else
      {
        if (!(trackDescriptor is ElementMatcherTrackDescriptor))
          return;
        this.TriggerElement(((ElementMatcherTrackDescriptor) trackDescriptor).Item, currentPageContext);
      }
    }
  }

Taking a look at the processor, SCBeacon.trackEvent does exactly what it says and tracks specific actions but does not handle any contact identifications or contact updates.

The next step is to create the custom process by extending (inheriting) the Sitecore default TriggerEventsProcessor with the following code:

public class TriggerFXMEventsProcessor : Sitecore.FXM.Pipelines.Tracking.TriggerPageEvent.TriggerEventsProcessor
    {
        //private readonly IAnalyticsDefinitionsRepository _analyticsDefinitionsRepository;
        public TriggerFXMEventsProcessor(IAnalyticsDefinitionsRepository analyticsDefinitionsRepository) : base(analyticsDefinitionsRepository)
        {
        }
        public new void Process(TriggerPageEventArgs args)
        {
            IPageContext currentPageContext = args.CurrentPageContext;
            Assert.IsNotNull((object)currentPageContext, "The current page is not tracked in the current session. No events can be triggered.");
            ITrackDescriptor trackDescriptor = args.TrackDescriptor;
            if (trackDescriptor is EventTrackDescriptor)
            {
                EventTrackDescriptor eventTrackDescriptor = (EventTrackDescriptor)trackDescriptor;
                if (eventTrackDescriptor.Definition.Id.Equals(new Guid(("{A6202545-323F-42ED-B904-81079C762524}")))
                {
                    if (Sitecore.Analytics.Tracker.Current != null && Sitecore.Analytics.Tracker.Current.Contact != null)
                    {
                        UpdateandIdenifyContact(eventTrackDescriptor);
                    }
                    else
                    {
                        Log.Info("cannot found the contact for the current interaction", this);
                    }
                }
                else
                {
                    this.TriggerPageEvent((IDefinition)eventTrackDescriptor.Definition, currentPageContext, eventTrackDescriptor.Data, eventTrackDescriptor.DataKey, eventTrackDescriptor.Extras);
                }
            }
            else if (trackDescriptor is GoalTrackDescriptor)
            {
                GoalTrackDescriptor goalTrackDescriptor = (GoalTrackDescriptor)trackDescriptor;
                this.TriggerPageEvent((IDefinition)goalTrackDescriptor.Definition, currentPageContext, goalTrackDescriptor.Data, goalTrackDescriptor.DataKey, goalTrackDescriptor.Extras);
            }
            else if (trackDescriptor is CampaignTrackDescriptor)
            {
                CampaignTrackDescriptor campaignTrackDescriptor = (CampaignTrackDescriptor)trackDescriptor;
                currentPageContext.TriggerCampaign(campaignTrackDescriptor.Definition);
            }
            else if (trackDescriptor is OutcomeTrackDescriptor)
            {
                OutcomeTrackDescriptor outcomeTrackDescriptor = (OutcomeTrackDescriptor)trackDescriptor;
                OutcomeData outcome = new OutcomeData(outcomeTrackDescriptor.Definition, outcomeTrackDescriptor.CurrencyCode, outcomeTrackDescriptor.Monetary);
                IDictionary<string, string> extras = outcomeTrackDescriptor.Extras;
                if (extras != null && extras.Count > 0)
                    outcome.CustomValues["FXM_CustomValues"] = (object)outcomeTrackDescriptor.Extras;
                currentPageContext.RegisterOutcome(outcome);
            }
            else
            {
                if (!(trackDescriptor is ElementMatcherTrackDescriptor))
                    return;
                this.TriggerElement(((ElementMatcherTrackDescriptor)trackDescriptor).Item, currentPageContext);
            }
        }

        private void UpdateandIdenifyContact(EventTrackDescriptor eventTrackDescriptor)
        {
            try
            {
                Assert.IsTrue(eventTrackDescriptor.Extras.Any(), "FXM JS API - Parameter not passed");
                if (eventTrackDescriptor.Extras != null && eventTrackDescriptor.Extras.Any())
                {
                    var values = eventTrackDescriptor.Extras;
                    Assert.IsTrue(values.ContainsKey("uniqueId"), "FXM JS API - Parameter - uniqueId not passed");
                    Assert.IsNotNullOrEmpty(values["uniqueId"], "FXM JS API - Parameter - uniqueId in Empty");
                    if (values.ContainsKey("uniqueId") && !string.IsNullOrEmpty(values["uniqueId"]))
                    {
                        using (XConnectClient client = SitecoreXConnectClientConfiguration.GetClient())
                        {
                            ContactExpandOptions Facets = new ContactExpandOptions(new string[] {
                            PersonalInformation.DefaultFacetKey,
                            EmailAddressList.DefaultFacetKey,
                            PayLoad.DefaultFacetKey
                            });
                            Sitecore.XConnect.Contact contact = client.Get(new IdentifiedContactReference("ContactPayload", values["uniqueId"].ToString().ToLowerInvariant()), Facets);
                            PersonalInformation PersonalFacet;
                            EmailAddressList EmailAddressListFacet = null;
                            PayLoad PayLoadFacet = null;
                            var manager = Sitecore.Configuration.Factory.CreateObject("tracking/contactManager", true) as Sitecore.Analytics.Tracking.ContactManager;
                            if (contact == null) //if the contact is not existing in the system
                            {
                                contact = new Sitecore.XConnect.Contact(new ContactIdentifier("ContactPayload", values["uniqueId"].ToString().ToLowerInvariant(), ContactIdentifierType.Known));
                                client.AddContact(contact);
                                PayLoadFacet = new PayLoad();
                                FillPayload(PayLoadFacet, eventTrackDescriptor);
                                if (!string.IsNullOrEmpty(PayLoadFacet.FirstName))
                                {
                                    PersonalFacet = new PersonalInformation()
                                    {
                                        FirstName = PayLoadFacet.FirstName,
                                        LastName = PayLoadFacet.LastName
                                    };
                                    client.SetFacet(contact, PersonalInformation.DefaultFacetKey, PersonalFacet);
                                }
                                if (!string.IsNullOrEmpty(PayLoadFacet.Email))
                                {
                                    EmailAddressListFacet = new EmailAddressList(new EmailAddress(PayLoadFacet.Email, true), "Preferred");
                                    client.SetFacet(contact, EmailAddressList.DefaultFacetKey, EmailAddressListFacet);
                                }
                            }
                            else
                            {
                                PayLoadFacet = contact.GetFacet(PayLoad.DefaultFacetKey);
                                if (PayLoadFacet == null)
                                    PayLoadFacet = new PayLoad();
                                FillPayload(PayLoadFacet, eventTrackDescriptor);
                                if (!string.IsNullOrEmpty(PayLoadFacet.FirstName))
                                {
                                    PersonalFacet = contact.GetFacet(PersonalInformation.DefaultFacetKey);
                                    if (PersonalFacet == null)
                                    {
                                        PersonalFacet = new PersonalInformation()
                                        {
                                            FirstName = PayLoadFacet.FirstName,
                                            LastName = PayLoadFacet.LastName
                                        };
                                    }
                                    else
                                    {
                                        PersonalFacet.FirstName = PayLoadFacet.FirstName;
                                        PersonalFacet.LastName = PayLoadFacet.LastName;
                                    }
                                    client.SetFacet(contact, PersonalInformation.DefaultFacetKey, PersonalFacet);
                                }
                                if (!string.IsNullOrEmpty(PayLoadFacet.Email))
                                {
                                    EmailAddressListFacet = contact.GetFacet(EmailAddressList.DefaultFacetKey);
                                    if (EmailAddressListFacet == null)
                                        EmailAddressListFacet = new EmailAddressList(new EmailAddress(PayLoadFacet.Email, true), "Preferred");
                                    else
                                        EmailAddressListFacet.PreferredEmail = new EmailAddress(PayLoadFacet.Email, true);

                                    client.SetFacet(contact, EmailAddressList.DefaultFacetKey, EmailAddressListFacet);
                                }
                            }
                            client.SetFacet(contact, PayLoad.DefaultFacetKey, PayLoadFacet);
                            client.Submit();
                            manager.RemoveFromSession(Tracker.Current.Session.Contact.ContactId);
                            Tracker.Current.Session.IdentifyAs("ContactPayload", values["uniqueId"].ToString().ToLowerInvariant());
                            Tracker.Current.Session.Contact = manager.LoadContact(Tracker.Current.Contact.ContactId);
                            Log.Info("FXM JS API added contact "+ Tracker.Current.Contact.ContactId.ToString() + " for " + values["uniqueId"].ToString().ToLowerInvariant(), "FXM JS API");
                        }
                    }
                    else
                    {
                       Log.Error("FXM JS API - Parameter - uniqueId not passed", "FXM JS API");
                    }
                }
                else
                {
                    Log.Error("FXM JS API - Parameters not passed", "FXM JS API");
                }
            }
            catch (XdbExecutionException ex)
            {
                Log.Error("XDB Exception while adding or updating contacts " + ex.Message, ex, this);
                throw ex;
            }
            catch (System.AggregateException ae)
            {
                foreach (var e in ae.Flatten().InnerExceptions)
                {
                    Log.Error("Aggregate Error while adding or updating contacts " + e.Message, e, this);
                }
                throw ae;
            }
            catch (Exception ex)
            {
                Log.Error("Exception while adding or updating contacts " + ex.Message, ex ,this);
                throw ex;
            }
        }
        private void FillPayload(PayLoad payloadinfo, EventTrackDescriptor eventTrackDescriptor)
        {
            var values = eventTrackDescriptor.Extras;
            payloadinfo.UniqueId = values.ContainsKey("uniqueId") ? values["uniqueId"].ToString().ToLowerInvariant() : string.Empty;
            payloadinfo.CustomerName = values.ContainsKey("customerName") ? values["customerName"].ToString() : string.Empty;
            payloadinfo.FirstName = values.ContainsKey("firstName") ? values["firstName"].ToString() : string.Empty;
            payloadinfo.LastName = values.ContainsKey("lastName") ? values["lastName"].ToString() : string.Empty;
            payloadinfo.Email = values.ContainsKey("email") ? values["email"].ToString() : string.Empty;         
            payloadinfo.Language = values.ContainsKey("language") ? values["language"].ToString().ToLowerInvariant() : "en";
        }
    }

As you could see that even though I have inherited the Sitecore default TriggerEventsProcessor, I have used Process() method as is, instead of overriding with a check for the event’s definition as CONTACTEVENT({A6202545-323F-42ED-B904-81079C762524}) to update and Identify Contact. This will make sure that the default page event is not triggered when there is a huge amount of call to update contact via trackevent of beacon script from an external site.
The next step is to register the custom processor in Sitecore configuration using Sitecore config patching standard practice.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" >
 <sitecore>
  <pipelines>
   <group name="FXM" groupName="FXM" >
    <pipelines>
     <tracking.triggerpageevent>
      <processor type="Project.Standalone.FXM.Pipelines.Tracking.TriggerFXMEventsProcessor, Project.Standalone" patch:instead="processor[@type='Sitecore.FXM.Pipelines.Tracking.TriggerPageEvent.TriggerEventsProcessor, Sitecore.FXM']" resolve="true"/>
     </tracking.triggerpageevent>
    </pipelines>
   </group>
  </pipelines>
 </sitecore>
</configuration>

Once the custom processor is registered, we need to create the CONTACTEVENT ({A6202545-323F-42ED-B904-81079C762524}) page event items in Sitecore. Navigate to /Sitecore/system/Settings/Analytics/Page Events path in the content tree and create page event as described below.
Contact Event

Now we can call the trackevent method after the load of the beacon script. There are different ways (refer to Sitecore documentation) to pass the updated contact data to the page event, it also depends on your implementation. I decided to use an extra dictionary of EventTrackDescriptor to send the contact details. To test you can call the trackEvent method in the developer console of your browser, once FXM script is loaded on the External Site.

SCBeacon.trackEvent("contactevent", {
                     xuniqueId: "0001181764-288089",
                     xcustomerName: "NICOLE TREJOS",
                     xfirstName: "NICOLE",
                     xlastName: "TREJOS",
                     xemail: "NICOLE.TREJOS@test.com",
                     xlanguage: "en"
                   });

You can see the new contact added using Experience Profile.
Experience Profile