If you've played around with the Experience Profile in Sitecore, you may have noticed the red bar at the top of the timeline with the label Unknown Contact. In this post I'll go into depth about this red bar--a feature in the Experience Profile timeline called eras--and how you can customize it in your Sitecore project.

Update (February 23, 2020)

All of the code in this post has been updated to work with Sitecore 9.3. See this repository on GitHub for a sample project: https://github.com/coreyasmith/sitecore-custom-timeline-eras. I've added tags to the repository for versions of the code that work with Sitecore 8.2 Update-4 and 8.2 Update-7, too.

What Is This Red Bar in the Experience Profile Timeline?

The Experience Profile timeline shows historical information about a customer's journey with your site: when the customer first visited your site, when the customer triggered goals, outcomes, or campaign events, etc. Starting with your customer's first visit to your site, you'll see a red bar at the top of the Experience Profile timeline labeled Unknown Contact. This red bar represents the first era of your customer's engagement with your site as a new, unknown contact.

Eras represent evolutions in your customer's relationship with your brand. If your company has a rewards program, you might have eras such as New Member, Silver Member, Gold Member, or Platinum Member when your customer triggers certain outcomes in your system, such as registering as a new user or spending a certain amount of money on goods.

Rewards Program Eras

Timeline eras are tied directly to outcomes in Sitecore. Outcomes are a lot like goals, but typically have a monetary value associated with them. Also, unlike goals, outcomes can only be triggered programatically through code; there is no way to trigger outcomes out of the box. You can read more about outcomes on Sitecore's Outcome documentation.

Add New Eras

The first thing to note about outcomes is that not all of them create an era in the Experience Profile timeline. Sitecore comes with a handful of era-triggering outcomes out of the box: Marketing Lead, Sales Lead, Opportunity, Close - Won, Close - Lost, and Close - Cancelled. These can all be found in the Marketing Control Panel under the Outcomes node at /sitecore/system/Marketing Control Panel/Outcomes. What makes these outcomes show as eras is the fact that they are tagged with the outcome group Lead management funnel (item ID {605C0647-A77F-4BEB-AA92-00112AF582E7}).

Built-In Marketing Lead Outcome

If you want to add your own era-triggering outcomes out of the box, you must tag them with Lead management funnel. If you don't, you will see an icon for your outcome in the Experience Profile timeline when it is triggered, but no red bar. If you want outcomes with different tags to show up as eras in the Experience Profile timeline, read on!

The Anonymous Era

The timeline of every contact in the Experience Profile starts off with an era titled Unknown Contact that is tied to the contact's first interaction with your site, a.k.a., the anonymous era. This isn't actually an outcome, it's added on the fly to the timeline every time the timeline is rendered. You won't find any outcomes in the Marketing Control Panel named Unknown Contact, and it's not registered in xDB.

What surprised me about eras in the Experience Profile timeline (and ultimately led to this post) is the fact that identifying contacts with Tracker.Current.Session.IdentifyAs(source, knownIdentifier) doesn't create a new, Known Contact era to end the Unknown Contact era. After all, once a contact is identified, he/she is no longer an Unknown Contact.

Remove the Anonymous Era

If you don't want this era to appear on your timeline you can remove it with a pipeline processor. The processor is fairly simple:

using System.Data;
using System.Linq;
using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Contact.Journey;
using Sitecore.Cintel.Reporting.Processors;

public class RemoveAnonymousEra : ReportProcessorBase 
{
  public bool ShowIcon { get; set; }

  public override void Process(ReportProcessorArgs args) 
  {
    var resultTableForView = args.ResultTableForView;
    RemoveAnonymousEraFromTimeline(resultTableForView);
  }

  private void RemoveAnonymousEraFromTimeline(DataTable resultTable) 
  {
    var dataRows = resultTable.AsEnumerable();
    var anonymousEras = dataRows.Where(r => r.Field<string>(Schema.EraText.Name) == "Unknown Contact");
    foreach (var anonymousEra in anonymousEras.ToList())
    {
      if (ShowIcon)
      {
        anonymousEra.SetField(Schema.EventType.Name, "Outcome");
      }
      else
      {
        resultTable.Rows.Remove(anonymousEra);
      }
    }
  }
}

Patch it into the ExperienceProfileContactViews Journey pipeline right after the Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges processor:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="ExperienceProfileContactViews">
        <pipelines>
          <journey>
            <processor type="Your.Assembly.RemoveAnonymousEra, Your.Assembly"
                       patch:after="processor[@type='Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges, Sitecore.Cintel']">
              <showIcon>true</showIcon>
            </processor>
          </journey>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

And with that the Unknown Contact era disappears.

Removed Unknown Contact Era

If you want to remove the initial interaction icon as well, just set showIcon to false in the config patch above.

Removed Unknown Contact Icon

The Registered Member, Silver Member, Gold Member, and Platinum Member eras you see above are just outcomes I created in Sitecore tagged with the Lead management funnel outcome group.

Rename the Anonymous Era

If the fact that the Anonymous Era is named Unknown Contact doesn't suit your fancy, it's easy to change that through a pipeline processor, too:

using System.Data;
using System.Linq;
using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Contact.Journey;
using Sitecore.Cintel.Reporting.Processors;

public class RenameAnonymousEra : ReportProcessorBase
{
  public string AnonymousEraName { get; set; }

  public override void Process(ReportProcessorArgs args)
  {
    var resultTableForView = args.ResultTableForView;
    RenameAnonymousEraInTimeline(resultTableForView);
  }

  private void RenameAnonymousEraInTimeline(DataTable resultTable)
  {
    var dataRows = resultTable.AsEnumerable();
    var anonymousEras = dataRows.Where(r => r.Field<string>(Schema.EraText.Name) == "Unknown Contact");
    foreach (var anonymousEra in anonymousEras.ToList())
    {
      anonymousEra.SetField(Schema.EraText.Name, AnonymousEraName);
    }
  }
}

Patch it into the ExperienceProfileContactViews Journey pipeline right after the Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges processor:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="ExperienceProfileContactViews">
        <pipelines>
          <journey>
            <processor type="Your.Assembly.RenameAnonymousEra, Your.Assembly"
                       patch:after="processor[@type='Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges, Sitecore.Cintel']">
              <anonymousEraName>Window Shopper</anonymousEraName>
            </processor>
          </journey>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Here you can see the Unknown Contact era renamed to Window Shopper for this commerce-themed example:

Window Shopper Anonymous Era

Again, the Registered Member, Silver Member, Gold Member, and Platinum Member eras you see above are just outcomes I created in Sitecore tagged with the Lead management funnel outcome group.

Add New Eras with Custom Tags

The Lead management funnel outcome group might not make sense for your marketing taxonomy. In the examples above, perhaps a Rewards program outcome group would make more sense for those outcomes. If you want outcomes with custom groups to show up as eras in the Experience Profile timeline, you'll need some custom code. Fortunately it's not hard to create a flexible solution so that any outcome can be configured to show up as an era in the timeline without being tied to a specific outcome group.

Add Show as Era Field to Outcome Definition Template

First you will need a new checkbox field on your Outcome Definition items to indicate whether the item should be shown as an era. Add a new section to the Outcome Definition template (/sitecore/templates/System/Analytics/Outcome/Outcome Definition) called Experience Profile Options. Add a Checkbox field to this template called Show as Era as shown below:

Custom Outcome Definition Template

Update: I normally advocate against modifying out-of-the-box templates, and when I originally wrote this blog post for Sitecore 8 I showed an approach to create this field on a separate template. However, the Trigger Outcome submit action for Sitecore Forms introduced in Sitecore 9 is hard coded to only allow the selection of outcomes based on the out-of-the-box Outcome Definition template. Adding this field to the Outcome Definition template is easier than fixing that hard-coded behavior, so here we are.

Create Pipeline Processor for Custom Eras

The Experience Profile Timeline gets its data from the Journey pipeline in the ExperienceProfileContactViews pipeline group. To draw eras in the Experience Profile Timeline from Outcome Definitions with the Show as Era field checked, you have to add a new processor to this pipeline:

using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using Sitecore.Cintel.ContactService;
using Sitecore.Cintel.Reporting;
using Sitecore.Cintel.Reporting.Contact.Journey;
using Sitecore.Cintel.Reporting.Processors;
using Sitecore.Data.Fields;
using Sitecore.Marketing.Definitions;
using Sitecore.Marketing.Definitions.Outcomes.Model;
using Sitecore.XConnect;
using Sitecore.XConnect.Client;
using Sitecore.XConnect.Client.Configuration;

public class PopulateCustomEraChanges : ReportProcessorBase
{
  private static readonly ID ShowAsEraFieldId = new ID("{Your-Show-As-Era-Field-ID-Here}");

  public override void Process(ReportProcessorArgs args)
  {
    var resultTableForView = args.ResultTableForView;
    PopulateWithEraChanges(args.ReportParameters.ContactId, resultTableForView);
  }

  private void PopulateWithEraChanges(Guid contactId, DataTable resultTable)
  {
    var changingOutcomesFor = GetEraChangingOutcomesFor(contactId);
    foreach (var dataRow in resultTable.AsEnumerable())
    {
      var timeLineEventId = dataRow.Field<Guid?>(Schema.TimelineEventId.Name);
      if (!timeLineEventId.HasValue) continue;

      var contactOutcome = changingOutcomesFor.SingleOrDefault(o => o.Id == timeLineEventId.Value);
      if (contactOutcome == null) continue;

      var definition = OutcomeDefinitionManager.Get(contactOutcome.DefinitionId, CurrentCultureInfo);
      ConvertToEraChangeEvent(dataRow, definition);
    }
  }

  protected virtual IReadOnlyCollection<Outcome> GetEraChangingOutcomesFor(Guid contactId)
  {
    var allOutcomeDefinitions = OutcomeDefinitionManager.GetAll(CultureInfo.InvariantCulture);
    var allEraChangingOutcomes = allOutcomeDefinitions.Where(IsCustomEraChangingOutcome);

    var contact = GetContact(contactId);
    var contactOutcomes = contact.Interactions.SelectMany(i => i.Events.OfType<Outcome>());
    var eraChangingOutcomes = contactOutcomes.Where(co => allEraChangingOutcomes.Any(o => o.Data.Id == co.DefinitionId));
    return eraChangingOutcomes.ToList();
  }

  protected virtual Contact GetContact(Guid contactId)
  {
    using (var client = SitecoreXConnectClientConfiguration.GetClient())
    {
      var contactReference = new ContactReference(contactId);
      var contact = client.Get(contactReference, new ContactExpandOptions(Array.Empty<string>())
      {
        Interactions = new RelatedInteractionsExpandOptions
        {
          StartDateTime = DateTime.MinValue,
          Limit = int.MaxValue
        }
      });
      if (contact == null) throw new ContactNotFoundException($"No Contact with id [{contactId}] found.");
      return contact;
    }
  }

  protected virtual bool IsCustomEraChangingOutcome(DefinitionResult<IOutcomeDefinition> outcomeDefinition)
  {
    var definitionItem = GetItemFromCurrentContext(outcomeDefinition.Data.Id);
    var showAsEraField = (CheckboxField)definitionItem.Fields[ShowAsEraFieldId];
    var isCustomEraChangingOutcome = showAsEraField?.Checked ?? false;
    return isCustomEraChangingOutcome;
  }

  private static void ConvertToEraChangeEvent(DataRow outcomeRow, IDefinition outcomeDefinition)
  {
    outcomeRow.SetField(Schema.EventType.Name, Schema.TimelineEventTypes.EraChange);
    outcomeRow.SetField(Schema.EraText.Name, outcomeDefinition.Name);
  }
}

For this code to work, you'll need to make sure that you have the following NuGet packages added to your project through Sitecore's NuGet feed):

  • Sitecore.Cintel
  • Sitecore.Kernel
  • Sitecore.Mvc.Analytics

This processor is largely hacked together from the built-in Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges processor since all of its interesting methods are private. It finds all of the Outcome Definitions in the Marketing Control panel that have the custom Show as Era field checked, sees which of those the Experience Profile contact has triggered, and returns them as era-change events on the Experience Profile timeline.

Patch this processor right after the Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges processor in the ExperienceProfileContactViews Journey pipeline:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="ExperienceProfileContactViews">
        <pipelines>
          <journey>
            <processor type="Your.Assembly.PopulateCustomEraChanges, Your.Assembly"
                       patch:after="processor[@type='Sitecore.Cintel.Reporting.Contact.Journey.Processors.PopulateEraChanges, Sitecore.Cintel']" />
          </journey>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Create Some Outcome Definitions

With your custom field added, you can now create some Outcome Definitions in the Marketing Control Panel that will show up as eras on the Experience Profile timeline. Navigate to /sitecore/system/Marketing Control Panel/Outcomes, create an Outcome Definition, tag it with any Outcome Group, and check Show as Era as I have on the superhero-themed Outcome Definition below:

Superhero-Themed Outcome Definition

Now when this outcome is triggered it will show up in the Experience Profile timeline as an era:

Bruce Wayne's Experience Profile Timeline

Miscellany

Here are a few observations I made while working on this blog post:

  • Era-changing outcomes have a larger icon in the Experience Profile timeline than regular outcomes.
  • You can change the icons of your outcomes in the Experience Profile timeline by adding an image to the Image field of your Outcome Definition items.
  • You don't need to publish your Outcome Definition items to see changes to the names or images in the Experience Profile timeline--those changes will be reflected immediately after saving the item.
  • If you have outcomes that should only be able to be triggered once then make sure that you check the Ignore Additional Registrations field on your Outcome Definition items. Era-changing outcomes are good candidates for this.
  • There is a bug with the Experience Profile timeline where it fails to render if the contact has multiple events/outcomes registered at the same time. Reach out to Sitecore Support about this patch for your instance to avoid that bug: https://github.com/SitecoreSupport/Sitecore.Support.126998.134727.

Let me know your thoughts in the comments.