Quantcast
Channel: Exercising Sitecore
Viewing all 29 articles
Browse latest View live

Index custom xDB Facets to Segment Contacts in Sitecore’s List Manager

$
0
0

It’s a new year! And fresh from a short break from the blog, to focus on presenting at the xDB training course in New Orleans and Sitecore User Group in Cardiff, I’m back. So let’s get to it!

I talk a lot about the value of extending xDB to store data relevant to the client and how to surface that data for Content Editors to use. I now want to cover how to use that data on a grand scale. From accessing that data for all your contacts in a performant way. Then using that data to create lists of similar contacts based on their interactions. Finally allowing you to target groups of individuals with relevant email communications, via EXM, or synchronize with a CRM (more on that in a future blog).

Note :
This functionality is only available in Sitecore 8.1 Update 3 and upwards

Indexing xDB Facet Data

We’re dealing with potentially hundereds of thousands of Contacts stored in xDB, each one having a plethora of data stored against it. Computing that quantity of data at run time each and every time isn’t going to cut it. Fortunately, this is not a new problem. We deal with this challenge when searching for Content. So we can use indexes to compute the large amount of data at a deferred time and indexing only what we need.

Fortunately, the Sitecore Analaytics Index is already indexing Contact data stored in the default xDB Facets and from Sitecore 8.1 Update 3 upwards we can now tell it to index our custom facets.

As expected, there is a pipeline for us to hijack and get it to do our bidding; contactindexable.loadfields. We’ll use this pipeline to access data in xDB Facets of the Contact currently being indexed and then return the data we care about in a number of indexable data fields. First the patch for the pipeline.

Now that we’re hooked into the pipeline for getting the indexable fields lets create a class which inherits from ContactIndexableLoadFieldsProcessor to get the data. The overridable GetFields method accepts ContactIndexableLoadFieldsPipelineArgs which exposes the Contact. From there you can call GetFacet method to retrieve your custom Facet.

If you are familiar with my previous blogs the example I use is a client’s site allows customers to order samples of products. I store contextual information about that sample order in xDB.  I want to index all skus of the products that have been ordered by the Contact as well as their favourite (most ordered) product type.

In the code below, once I have calculated the skus and favourite type I create a new IndexableDataField for each, defining the field name, the value, and the type. For the favourite product type I simply want to store the type’s name as a string. Whereas for the products ordered I want to store each product’s sku in an array of strings.

I recommend you create a processor for each type of data you want to index. In my example I’m only indexing one Element in one of my custom Facets.

Now that we have done the hard work of retrieving the values and creating indexable fields we need to define those fields on the Sitecore Analytics Index. As always I recommend using a patch. Add new fields within the Field section defining the FieldName property as the name you gave it in the processor.

Here is the field definitions for Lucene.

Here is the definition for your next indexable fields if you are using Solr.

Using Luke for Lucene you can see that the data of the custom Facet for this Contact has been indexed.

Index custom Sitecore xDB Facets

Quick bit of help – Once the new index patches are added to your site it will likely begin indexing the Contacts. If not, try visiting as a new contact and push the session to xDB. Reminder, you can’t rebuild the Sitecore_Analytics_Index like you can other indexes.

Now that we have an efficient method of selecting any number of contacts based on xDB data lets put it to use.

List Manager – Custom Segmentation Rules

Let’s say I want to group every contact based on their favourite type of product or I want to email promotional materials to all Contacts who have ordered samples from a particular range, then I am segmenting Contacts. Sitecore’s List Manager is made for this purpose. However the List Manager is not aware of our custom xDB data now indexed in the Analytics index. Let’s make it aware.

The List Manager lets you select Contacts based on a number of rules, this is the same Rules Engine that is used for personalisation etc. However Segmentation rules can’t be used for personalisation and vice versa.

To select the contacts in the above scenarios I will need to create a custom Rule Condition class. The class must inherit from the base class TypedQueryStringOperatorCondition of the IndexedContact type and a generic type where the generic type inherits from VisitorRuleContext. This exposes the IndexedContact allowing retrieval of the various IndexedDataFields including the ones we’ve created. As we are inheriting from TypedQueryStringOperatorCondition we take a value defined by the Content Editor and run compare expressions against the value we have indexed.

Before we can run the rule we need to create an definition Item in Sitecore. Insert a new Item at the path /sitecore/system/Settings/Rules/Definitions/Elements/Segment Builder/ using the Condition template. Enter the namespace for the type Field and then define the text of the rule.

Custom Segmentation rule

Upon selecting the rule the Content Editor is prompted to chose a comparison type and the value to compare. I recommend writing your rules such that they support the built-in operators but if your indexed data is more complex I recommend hiding the comparison types away from the users.

Sitecore Segmentation rule

Upon clicking OK the list is populated with with Contacts matching the conditions and all contacts will be visible within the List Manager.

Contacts in List Manager

And that’s it!

Segmenting millions of contacts in meaningful ways done simply, elegantly and no nasty load on the server. You’ll be free to use all that great data you are capturing in new and helpful ways.

The lists themselves are used by Sitecore’s EXM so you are facilitated tailored mailshots right off the bat. From the List Manager you can access each Contact’s Experience Profile, which makes the List Manager the most effective way to find particular Contacts right now.

The lists a Contact belongs is also stored against the Contact record in xDB so they can be easily accessed for other uses. Such as sending particular Contacts to a CRM I’ll be covering this in a future blog.



How to find Sitecore Contacts in MongoDB

$
0
0

Fed up of trying to your Contact in 100,000s of Contacts in xDB when you only want see if some data is there. Yeah me too. Here are the easiest way to find your Contacts.

First things first, lets get the Contact’s Id. To do that in your browser access the cookies for your site. You should find a cookie named to the effect of SC_ANALYTICS_GLOBAL_COOKIE. That cookie will have a guid which is the Id of the Contact you are currently viewing the site as. The cookie value also has a bool separated by a pipe which I believe is relating to merging of Contacts (will confirm).

SC_Analytics_global_cookie contact id

Robomongo

You’ll likely we want to see the Contact data as its stored. Best way to do that is accessing MongoDB using a tool like Robomongo. Robomongo lets you see the BSON data directly;

Sitecore Contact in xDB

Sadly its not as simple as copying the Contact Id into the Mongo query. The Id needs to have a particular encoding. Fortunately you can do this pretty simply in C# by converting the guid to bytes and then converting the bytes to a Base64String.

Make this easy for me

No one has time to run another solution in Visual studio just to convert some Ids. Therefore I’ve written a .NET Fiddle to do the conversion for you. Pass in your contact Id, run it and it spits out an Id you can use in Robomongo to find your contact. I’ve also included the query to run If you’re feeling particularly lazy.

Parsed contact Id:
L45jULJD7kaAbWZfT7CJVA==

Robomongo query:
db.getCollection('Contacts').find({_id:new BinData(3, 'L45jULJD7kaAbWZfT7CJVA==')})

Much easier than having a separate sln to fire up. You can even Fork it so you can pass in a multiple if you’re looking for a number of Contacts.

Web Api

As the Contact data displayed in the Experience Profile is served via Web Api you can actually make a web request to view the data using the Contact Id.

http:///sitecore/api/ao/v1/contacts/

e.g https://jonathanrobbins.co.uk/sitecore/api/ao/v1/contacts/50638e2f-43b2-46ee-806d-665f4fb08954

Returning you a JSON object of all the data stored in xDB. You can even narrow down to particular xDB Elements you want to see data for.

http:///sitecore/api/ao/v1/contacts//intel/

e.g https://jonathanrobbins.co.uk/sitecore/api/ao/v1/contacts/50638e2f-43b2-46ee-806d-665f4fb08954/intel/sampleorders

Fortunately the Url requires authentication on the Sitecore domain before the data is displayed so not something public users can get their hands on.

Experience Profile

Finally you can go the Experience Profile route for a nice UI to go with it. Even if your Contact is not findable by the great Experience Profile search.. (as it may not be added to the Sitecore Analytics Index yet and the search is massively lame). Just add your Contact ID to the Experience Profile Contact Url to see the data in xDB. This isn’t cached data either as its using Web Api to talk to xDB directly.

http:///sitecore/client/Applications/ExperienceProfile/contact?cid=

And that’s it!

Three easy ways to view to find a particular Contact’s data stored directly in xDB. Saving you from trawling through pages and pages of Contacts in Robomongo trying to guess based on the number fields the Contact has.

I also recommend checking out MongoDb’s tutorial on queries for when you come to do more complex searches. Really helped me understand when it came to some custom reporting I have done against xDB.


Sitecore – Data at the root level is invalid. Line 1, position 1.

$
0
0

I’ve encountered this error message on various Sitecore implementations predominantly while publishing. It’s painful as its fatal to publishing process and Content Editors get uppity when they see the stack trace in the Publishing window.

ERROR Catching Exception in Global.asax_Application_Error
Exception: System.Web.HttpUnhandledException
Message: Exception of type 'System.Web.HttpUnhandledException' was thrown.
Source: System.Web
at System.Web.UI.Page.HandleError(Exception e)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Nested Exception

Exception: System.Reflection.TargetInvocationException
Message: Exception has been thrown by the target of an invocation.
Source: mscorlib
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Sitecore.Reflection.ReflectionUtil.InvokeMethod(MethodInfo method, Object[] parameters, Object obj)
at Sitecore.Shell.Applications.ContentManager.ContentEditorPage.OnPreRender(EventArgs e)
at System.Web.UI.Control.PreRenderRecursiveInternal()
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Nested Exception

Exception: System.Xml.XmlException
Message: Data at the root level is invalid. Line 1, position 1.
Source: System.Xml
at System.Xml.XmlTextReaderImpl.Throw(Exception e)
at System.Xml.XmlTextReaderImpl.ParseRootLevelWhitespace()
at System.Xml.XmlTextReaderImpl.ParseDocumentContent()
at System.Xml.Linq.XDocument.Load(XmlReader reader, LoadOptions options)
at System.Xml.Linq.XDocument.Parse(String text, LoadOptions options)
at Sitecore.Data.Fields.PagePreviewField.ParseValue(String value)
at Sitecore.Data.Fields.PagePreviewField..ctor(Field innerField)
at Sitecore.ApplicationCenter.PagePreview.PagePreviewManager.GetPreviews(Item item)
at Sitecore.Shell.Framework.Commands.ContentEditor.PagePreviews.GetHeader(CommandContext context, String header)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.FillParamsFromCommand(CommandContext commandContext, RibbonCommandParams ribbonCommandParams)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.GetCommandParameters(Item controlItem, CommandContext commandContext)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderLargeButton(HtmlTextWriter output, Item button, CommandContext commandContext)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderButton(HtmlTextWriter output, Item button, CommandContext commandContext)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderChunk(HtmlTextWriter output, Item chunk, CommandContext commandContext)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderChunk(HtmlTextWriter output, Item chunk, CommandContext commandContext, Boolean isContextual, String id)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderChunk(HtmlTextWriter output, Item chunk, CommandContext commandContext, Boolean isContextual)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderChunks(HtmlTextWriter output, Item strip, CommandContext commandContext, Boolean isContextual)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderStrips(HtmlTextWriter output, Item ribbon, Boolean isContextual, ListString visibleStripList)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.RenderStrips(HtmlTextWriter output, Item defaultRibbon, Item contextualRibbon, ListString visibleStripList)
at Sitecore.Web.UI.WebControls.Ribbons.Ribbon.Render(HtmlTextWriter output)
at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
at Sitecore.Web.HtmlUtil.RenderControl(Control ctl)
at Sitecore.Shell.Applications.ContentManager.ContentEditorForm.UpdateRibbon(Item folder, Boolean isCurrentItemChanged, Boolean showEditor)
at Sitecore.Shell.Applications.ContentManager.ContentEditorForm.Update()
at Sitecore.Shell.Applications.ContentManager.ContentEditorForm.OnPreRendered(EventArgs e)

Cause of the problem

From the numerous times I’ve had this problem its due to one or more Items having invalid or inaccurate data in one or more of their Fields. For example;

  • Invalid XML in the rendering fields
  • Plain text in a General Link field
  • A field’s raw data being an incorrect structure
  • Problematic character in a field (á, ¿, etc)
  • Preview field referencing a non-existent version of the Item

The latter happened most recently and definitely the most painful to identify.

Identifying the source

The first step is to identify which item(s) are causing the fatal error to the publish. Sitecore logging does record the error in both the logs and the publishing logs but neither state which item it failed on. Awesome. If you have a vague idea which item(s) are the problem then knowing which to check shouldn’t be so bad. If not. The hunt is on.

Chances are you don’t have time to write a Sitecore Powershell script to traverse the tree and report which item failed to publish (I may actually get around to this one day). The most effective, but brute force, way I use is divide and conquer the tree.

Smart publish each item under the Sitecore root node in turn (Content, Layout, Templates, etc) not forgetting to check subitems. When the error occurs during publish repeat the steps for the Item you just published children. Eventually you should narrow it down to a folder or group items.

Now to find the individual item. In my most recent case the Content Editor failed to load the Item when clicking on it in the tree. A dead give away. More on why it didn’t open later. However, more often than not your Item will open in the Content Editor and you will need to try to identify the raw value in the field of the items that is causing a problem.

That can be a pretty painful task and not an option for me when the Item won’t even open. How can you address the problem if you can’t access the item?

Sitecore’s DB Browser

Sitecore has an admin page out of the box that effectively lets you browse the content tree and see the values as they’re stored in the database. Think of it as a really stripped back version of the Content Editor.

Sitecore admin dbbrowser

Access the database browser via www.yoururl.com/sitecore/admin/dbbrowser.aspx and login with an admin account.

As the dbbrowser is bare bones, feature errors and UI exceptions are kept to a minimum. So in my scenario I could open my problematic item. It also shows the fields in a manner that’s easier to compare, showing the raw data, and flipping between items is almost instantaneous. Making it perfect tool to resolve problems with field data.

Resolving the problem

As mentioned earlier your problem is likely due to special characters, invalid xml and/or invalid data in fields. You should be able to address the first two quite easily. The last one may be more tricky as some Sitecore fields require the raw data to have a particular structure. I recommend comparing the values to values of an item that does publish.

I have read a few posts saying that this exception can be caused by broken links. So use the Broken Links Manager to see if these items reference deleted items. It hasn’t been the source for my scenarios but worth investigating.

Now it may not be any of those above problems, like in my case. So I recommend alternating the view between a publishable item and the unpublishable one. As you browse the fields you should be able to spot differences. Address these differences and publish again.

In my case, the unpublishable Item had the value ‘3’ in the Preview standard field. All others did not. Turns out the Preview field was referencing a version of the Item that did not exist. Removing that value allowed me to open the Item in the Content Editor as well as address the publishing issue.

Not hundred percent certain how the Preview field is used. An educated guess is that it outlines which version of the Item should be opened when previewing. Regardless problem solved with no side-effects.

And that’s it

Hopefully this post has gone some way to helping some of you out when you encounter this issue especially since the log message gives little to go on.

I’m back from my little blogging hiatus so expect the usual regular posts from me.

For more on the massively helpful admin pages Sitecore checkout this great post by Nikola Gotsev.

Sitecore – Pay your technical debts and move to Helix

$
0
0

At a recent Sitecore UK User Group Wesley Lomax and I presented on how to resolve technical debts in Sitecore implementations. With the release of the Helix principles last year a lot of preexisting Sitecore implementations are now facing the challenge of moving to these principles but are hindered by a load of technical debt.

For enterprise level implementations, it’s going to feel like an insurmountable challenge –  refactoring and correcting a colossal codebase and it’s Information Architecture (IA). It’s all probably been in development for years by multiple developers with multiple design patterns. On top of that add technical and business constraints, maintaining Business-as-usual, and still delivering projects? Fixing and updating legacy implementation soon seems like an impossible task.

I recently moved from agency to client-side this year and I have tackled this exact problem so I wanted to share some of my experiences to help others out.

First off, why should I make work for myself?

Here are a few benefits of solving your technical debt;

  • Raise the quality limit – if the foundations aren’t sound, what’s build on top can only be so good.
  • Code decay – the code’s environment changes and progresses (KPIs, infrastructure, audience, etc). If the code doesn’t change with it, it becomes less suitable, eventually a hindrance and ultimately becoming legacy code.
  • Short term pains for long term gains – the upfront work is going to be significant but will get more reasonable as time goes on.
  • Preempt serious rework – I’ve seen legacy code block entire projects let alone make development tasks longer.
  • Align with the direction of the industry – you’re going to be in a better position to take advantage of what Sitecore offers.
  • Improve as a developer – probably the most interesting to you, learn some new technologies, concepts, ways of working.
  • Undeveloping is fun! – ‘undeveloping’ some badly implemented code is favourite part of being a dev

The thing is, clients (usually) don’t want to pay to resolve technical debt. They see technical debit as just that, technical debt. It’s the developers remit. The more digitally mature clients will understand the cost of ownership; longer to implement changes, features more prone to bugs, performance issues etc.

However, just because the client doesn’t want to pay or devote time to technical debt it doesn’t mean it shouldn’t be resolved. So it’s down to you. You’re the one working with this thing right?

You’ll want to figure out what’s right for you

Don’t go blindly following Helix.

There is far more to building and updating a Sitecore implementation than implementing Helix principles. Make a few notes about your team, the client, and the existing implementation before deciding what you want to achieve. Here’s somethings you might want to consider;

  • Think about your team – we have a number of backend developers, frontend developers and architects with varying skill-levels. While we wanted the implementation to use the latest and greatest. The higher priority was to ensure all can work on the solution effectively. Simple solutions for complex problems.
  • Keep the Content Editors in mind – we have a Digital Team which are the site administrators and around hundred Content Editors. Some of whom use Sitecore everyday and others once every few months. So the IA changes need to be to intuitive but still familiar.
  • Whats the scope of your implementation – developing for an international commerce site requires completely different development considerations than a brochureware site. Whats a good technical approach for one could be disastrous for the other. Our implementation caters for half a million paying members reaching a hundred-thousand unique visitors each week. Performance and resilience is number one concern where personalisation and sales funneling might be for a brochureware site.
  • Be mindful of what the client actually does – while what the client sells or the services it provides may not have direct impact on what you’re going to fix first, it can give you an idea of what projects and features your implementation may have in the future. I have the complete re-platform of the My Account functionality coming so I needed to ensure the solution and infrastructure an handle heavy Microsoft Dynamics integration.

Agree as a team where you want to be

Make development great again Sitecore Jonathan Robbins

We just wanted to make development great again. Enjoying the day-to-day and not feel like we are fighting the code base to get something done.

As a team decide how you want the solution to look in three or six months time. What problems do you guys face daily? How pain-free is creating releases? What issues are always raised in support? Here a few things you might want to solve;

  • Daily pains – headaches you have to deal with while going about your day-to-day development.
  • Best practices – areas where the solution falls short of .NET and Sitecore best recommendations.
  • Solution structure – should the solution be easier to navigate or simple to utilise.
  • Development blockers – code or design choices that you are forced to work with or work around.
  • Methodologies – what design patterns or principles do you want to follow to improve the quality and ease of development.
  • Reoccurring problems – issues keep getting swatted only to reappear.
  • Logs – use the Sitecore Log Analyser to find the most frequent problems in your code base.
  • Resolve To Dos – all solutions have them.
  • Helix – complete a gap analysis of the solution against the Helix documentation to give you a shopping list of things to do. Do this for both Code and IA.

To give you an idea of the tech debt we wanted to solve;

  • Harden the foundations – ensure stability and performance of the site. Address the poor code implementation of MVC lowering the quality limit and enforce object-oriented design throughout.
  • Remove N Tier Architecture – N Tier was forcing us to dev in a particular way, building upon bad practice for consistency sake. Circular references was forcing bad design decisions. Finally we didn’t want monolithic architecture as we didn’t know where to start correcting. We wanted to move to Modular architecture.
  • Solve daily headaches – it was incredibly frustrating that the folder structure of Rendering Items was completely different from the Views. The repeated hunt for the razor files was infuriating.
  • Rewrite the bad – critical parts of the implementation were written by developers not quite familiar with Sitecore. We wanted to fix that. A lot of customisation simply wasn’t needed so we wanted it removed.
  • Enable future phases – restructure the solution ready for integrating with Dynamics to facilitate Membership Self-service. There is massive drive in the company to make use of all of Sitecore’s features and a lot of Sitecore’s contextual marking functionality didn’t work due to custom code or poor configuration.
  • Domain driven development – we wanted to be able to talk to our project owners in terms they understand. Meaning logical parts of the solution to be named and modeled after logical parts of the company and its services. Previously, all the projects were named after the technology it used.

How to go about it

1. Settle on technologies and design patterns

You know the problems you want to solve and how the solution should look. You probably have an idea of what technologies and approaches you can use to help you get there. If not, have a look at how Habitat handles tooling and I recommend SOLID Principles, clean coding, and the Stairway Pattern.  You want the code to be consistent, readable and easy to work with. Selecting principles can help you get there, especially with larger teams.

You’ll want a good Branching Strategy. The internet recommends so many different workflows but I don’t think you can go wrong with Git Flow. This should ensure your work in progress doesn’t some how make it to Prod.

Most of your time will likely be renaming, restructuring, removing, and refactoring code. Save yourself a headache. Get yourselves Resharper if you haven’t already.

Like us, you probably don’t have the time or the resource to regression test after each change. So I recommend you write some Spec Tests to cover the functionality, look at how Habitat has implemented them. This way you can ensure the User Story is still met while changing the underlying code. I don’t need to sell to you the value of Unit Tests.

If you’re going down the Modular route you’ll need to consider how you are going to publish all the dlls, views, and configs to your webroot. A Task runner like Grunt or Gulp (or some other random mouth noise) can collect those files up and place them in your local site. Modularisation of the code will affect your deployment packages too. So make sure you are adding the Projects as Build Dependencies of your Web App and reference them in your deployment package configs.

2. Rewrite development guidelines

It’s a good place to start.

Most companies have development guidelines that haven’t been looked at in years let alone updated. But what good is redeveloping existing code in a particular way if new code doesn’t follow suit? You just came up with the content of the development guidelines, take the time to draft up some paragraphs and examples.

You’re going to have a much easier time aligning everyone in the team. And newbies will have a handy guide on how to work on this thing.

Chances are, this will be the first time your jumble of ideas and approaches have been collected in one place. Writing it all down should show that your plan all fits together. But if it doesn’t, or there are some gaps, no worries! This is the perfect time to find something that works. Finding out after months of work is not.

Don’t just go copying the principles of Helix as your dev standards. You can state that your development guidelines also include Sitecore best practices. No point regurgitating.

3. Review your solution

By now you should have a pretty decent list of things you want to do. And with your Development Guidelines you have something to review the existing code against.

It may sound like a lot of work but it doesn’t need to be. You know how it should all be. So spending an hour or two making notes of things that need to be actioned. I recommend you pay special attention for things that can block your plans.

Ultimately this step is going to give you an idea of how the code matches up where you want it to be.

4. Review your Sitecore Items

This being a Sitecore implementation, the Sitecore Items are as important as the code files. Here are a few things I recommend doing.

Complete a Content Inventory. Sounds boring as holy hell. However, its good to understand what Templates are present, the data they model, the fields they have, and how the templates relate to each other. Chances are there are a number of templates / fields that aren’t required any more or can be merged.

It’s not just content though, chances are there are a number of Placeholder and Rendering Items that can be thrown away or need some rethinking. This work should actually help making code changes easier.

One thing that bugged me in my solution was that each of the 20-odd sites were structured differently. Where you’d find a datasource folder in one site, it would be located completely different in another. No concept of separating data that is shared across all sites or specific to one. So decide on the hierarchy of a Site. Map it out in Excel or something so you have a structure to work towards.

For this part, I found reviewing the IA against Helix Section’s 2.5 and 2.6 really helpful. Complete a gap analysis of how you stack up against it and aim to implement some of its conventions. I really liked the concept of an ‘Interface’ template. A template that doesn’t have any fields but including it an Item’s base Templates shows its of a particular type. For example, _Home Interface template added to the base templates of all Home Items is massively helpful to develop against if the various Home pages of your sites use different Templates.

5. Create a backlog

Your reviews would have given you a list of things to do. Basically you’ve got yourself a backlog. I’m not saying start running sprints and treating it like a project. But add the tasks to a Kanban board, I recommend Trello so you can share it easily. Pick up the tasks as and when you’re able.

Kanban board

You probably have a lot to do so keep tracking the work simple as possible. Three columns, To-do, Doing, Done. At the very least you’ll know how the solution is progressing.

One thing I will say is to prioritise the tasks which block others. We couldn’t do a lot of what we wanted until we started breaking down the Business Layer into Feature Projects.

6. Finally, pay your debts in installments

You’re probably thinking this all well and good but I don’t have three months to spend just doing this. Neither did I. What I found best is to work on areas when you come into contact with them.

You’re never going to get this done as a Big Bang approach – all in one go after months of dev. Clients won’t pay for it. They won’t accept a halt to delivery. And neither do you want to deploy all this massive refactoring all one go, far too risky. Imagine the conflicts you’ll have on merge.

What you want is phased approach. Roll out these changes a little bit each release. We use the Boy Scout Rule – leave the code in a better state than you found it. As you come into code leave it more in line with your development guidelines than how you found it.

Delivering new functionality around registration? Spend ten minutes moving the logic from the Controller to the Accounts Feature. Creating a new listing? Take the time to split the existing indexes into Feature-centric ones. Or whatever may be in your backlog, you get the point.

It may not sound like much, but a handful of developers doing the Boy Scout Rule as they build over three months can really turn a solution around.

Benefits of our hard work

We collected some metrics before we started on our Tech Debt Backlog. We also took another set of readings about two months of completing tasks via the Boy Scout Rule. We used NDepends to help get some figures – its a tool that can estimate Technical Debt based on attributes of the solution. We didn’t take the results as gospel but it was nice to have an independent opinion.

NDepends technical debt

Before – After

The key take away is that before we started our backlog our solution was estimated at having 77 days of technical debt. 77 days of purely fixing problems. On the later reading NDepends quoted us 14 days of technical debt. I’m sure there are diminishing returns here on out but that is a massive jump for moving to a modular architecture, enforcing SOLID principles, and undeveloping.

For actually working on the solution. Yeah, there are still times where I’m holding my head in my hands in dismay. But those are becoming less frequent. The business layer is shrinking fast and being replaced with Feature and Foundation projects. Current count at sixty.

We actually know where we to start fixing things instead of fighting the web of project dependencies. The SOLID principles and Stairway pattern has helped a lot with that. We’re no longer building upon bad for consistency sake. We can even find Razor files based on the location of their Rendering Item! Templates actually make sense now and were making progress changing the IA so that each site is consistent, has its own content as well as a share of the global content.

Code weeds are becoming less common. We’re not trying to solve a simple support ticket only to loose three hours of development time and submit humongous pull requests to each other (…on a Friday).

I’m not saying we’re done by any means. We still have a lot of work to do. I still need to nuke every helper class from existence. But we’re closer to where we wanted to be.

And that’s it!

So, get your team onboard. Figure out whats right for you. Decide how you want the solution to look in six months. Choose the tech and principles. Review the solution. Load up the backlog in a Kanban board. And leave your code in a better state than you found it.

What I can’t stress enough is not to tackle this on your own. You need to your Team’s buy-in. Without Wes, we wouldn’t have achieved a faction of what we have so far.

It does feel like we’re making development great again.

 

Wes has kindly published the full presentation slides on his blog.

Advertisements

SUGCON 2018 Presentation – A Developer Pays their Debts

$
0
0

Earlier in the year I was fortunate enough to be selected to present at Sitecore User Group Conference, SUGCON.

I presented with Wesley Lomax on the thinking, approach, and execution of how we solved the technical debt in a multi-million pound Sitecore implementation.

The presentation takes what I have written in a previous blog post but shows the progress in the months, and projects, between that post was written and when we presented.

Any feedback or comments please let me know, enjoy.

Advertisements

Display the Context of a Sitecore Goal in the Experience Profile

$
0
0

For a while now I’ve been writing about collecting and displaying data unique to a client in Sitecore’s Experience Profile. What went hand in hand with that is the use of Sitecore Goals and Outcomes – giving a business value, sometimes monetary, to actions performed on the site. The Experience Profile currently shows the goals for each Contact.

However, beyond the goal being registered we don’t know the context in which the goal was met. Wouldn’t it be great if we knew WHAT was added to the basket when the ‘Add to Basket’ goal was registered?

In my post extending xDB to show custom data I described how we can get that contextual data into xDB. I.e. a JavaScript function to initiate when an action is made by a User. Retrieving the contextual information from the markup of the page. Finally passing the goal and goal context to a MVC Controller to register it.

For example, a directory listing for a business’ employees. A goal is registered when the User clicks the ‘Call Now’ or ‘Email Now’ button to communicate with that member of staff. We will register the ‘Called Employee’ or ‘Emailed Employee’ goal but we can also include which employee contacted and the phone number or email address used.

But the Experience Profile doesn’t show this valuable contextual information even though its in xDB. So let’s do something about that.

Extending the Goals ExperienceProfileContactViews Pipeline

Back to the old ExperienceProfileContactViews Pipeline Groups. This time, we’re going to extend the Goals section to include two new pipelines. First, a pipeline to add an additional column to the DataTable to hold the Goal’s Context. Secondly, a pipeline to fill that column with data retrieved from xDB via a new DataSourceQuery, which we’ll get to shortly.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactViews">
<pipelines>
<goals>
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.AddGoalDescriptionColumn, JonathanRobbins.DisplayGoalContext" />
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.FillGoalDescription, JonathanRobbins.DisplayGoalContext" />
</goals>
</pipelines>
</group>
</pipelines>
</sitecore>

With me so far? Good.

Adding the column is as simple as below but needs to occur once the GoalsDataTable has been constructed by the appropriate pipeline.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class AddGoalDescriptionColumn : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
if (args.ResultTableForView == null)
return;
args.ResultTableForView.Columns.Add(Schemas.GoalDescription.ToColumn());
}
}

Entering the context of the goal into that column is a little more work. Pulling apart the data in the QueryResult to get the goal context and entering it into our new column. This needs to occur after the existing pipeline which populates the goals with data from xDB.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class FillGoalDescription : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
DataTable resultTableForView = args.ResultTableForView;
Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", new object[] { args.ReportParameters.ViewName });
int i = 0;
foreach (DataRow row in resultTableForView.AsEnumerable())
{
var goalData = args.QueryResult.Rows[i].ItemArray[4];
if (goalData != null)
{
row[Schemas.GoalDescription.Name] = goalData;
}
i++;
}
}
}

The schema which files both reference is as follows


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


namespace JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals
{
public static class Schemas
{
public static ViewField<string> GoalDescription = new ViewField<string>("GoalDescription");
}
}
view raw

Schemas.cs

hosted with ❤ by GitHub

Retrieving Goal information from xDB

As you may know, the Goal Type is a PageEvent. PageEvents are stored in xDB against a Contact’s Interaction, not the Contact themselves. Which makes sense architecturally speaking. A Contact interacts with the site, that interaction includes a number of page views, a page view may result in a page event.

When registering a goal I use the Description property to hold the context of the goal. In this example, it would be the name of the Employee the user chose to contact and medium of the contact; phone number or email address. This is done by the following;


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class PageEventController : Controller
{
[System.Web.Mvc.HttpPost]
public JsonResult RegisterGoal(string goalId, string goalDescription)
{
Item eventItem = Sitecore.Context.Database.GetItem(goalId);
var goal = new PageEventItem(eventItem);
if (!Tracker.IsActive)
Tracker.StartTracking();
Sitecore.Analytics.Model.PageEventData eventData = Tracker.Current.CurrentPage.Register(goal);
eventData.Data = goal["Description"] + " " + goalDescription;
Tracker.Current.Interaction.AcceptModifications();
return Json(new PageEventRequestResult()
{
Success = true,
Message = "Successfully registered goal",
});
}
}

This Goal Description ends up in the Data property on the PageEventData type which is written to xDB (so we don’t have to worry about writing something custom there). So in order to retrieve it for our new column we need to query xDB. To do that we need to replace the existing Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals type with our own.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactDataSourceQueries">
<pipelines>
<goalsquery>
<processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.ReportingServerDatasource.Goals.GetGoals, JonathanRobbins.DisplayGoalContext" />
</goalsquery>
</pipelines>
</group>
</pipelines>
</sitecore>

Similar to the existing GetGoals type we are replacing, we are querying the Interactions Collection in xDB for entries matching the Contact’s Id we are currently viewing in the Experience Profile. We are asserting that at least one PageEvent exists in the Page.PageEvents property so we don’t get null PageEvents. Our query requests which Fields we want from the entry specifying the Pages_PageEvents_Data property which holds our goal context.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class GetGoals : ReportProcessorBase
{
private readonly QueryBuilder goalsQueryBuilder = new QueryBuilder()
{
collectionName = "Interactions",
QueryParms =
{
{
"ContactId", "@contactId"
},
{
"Pages.PageEvents.0", "{$exists:1}"
}
},
Fields =
{
"_id",
"ContactId",
"Pages_PageEvents_PageEventDefinitionId",
"Pages_PageEvents_DateTime",
"Pages_PageEvents_Data",
"Pages_Url_Path",
"Pages_Url_QueryString",
"Pages_PageEvents_Value",
"Pages_Item__id"
}
};
public override void Process(ReportProcessorArgs args)
{
DataTable goalsData = this.GetGoalsData(args.ReportParameters.ContactId);
args.QueryResult = goalsData;
}
private DataTable GetGoalsData(Guid contactId)
{
ReportDataProvider reportDataProvider = this.GetReportDataProvider();
Assert.IsNotNull((object)reportDataProvider, "provider should not be null");
return reportDataProvider.GetData("collection", new ReportDataQuery(this.goalsQueryBuilder.Build())
{
Parameters =
{
{
"@contactId", (object) contactId
}
}
}, new CachingPolicy()
{
NoCache = true
}).GetDataTable();
}
}
view raw

GetGoals.cs

hosted with ❤ by GitHub

Almost there

The Experience Profile being built with SPEAK means to finish this off we need to do a bit. Fortunately, it is a very small bit. We simply need to add a new Item based on the ColumnField template in the Goals ListControl so that our new column displays in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals entering a sensible HeaderText and a DataField value that matches our schema name.

The result of our hard work;

Sitecore Goals Context

And that’s it!

A good effective improvement to the Goals area of the Sitecore Experience Profile. Give your Content Editors the what, why and how for when the User achieved the goal.

If you like this but don’t fancy doing work I’ll be submitting this as a Sitecore Marketplace Module shortly so you can just take it from there. I will update this post soon as its available for all!

Display the Context of a Sitecore Goal in the Experience Profile

$
0
0

For a while now I’ve been writing about collecting and displaying data unique to a client in Sitecore’s Experience Profile. What went hand in hand with that is the use of Sitecore Goals and Outcomes – giving a business value, sometimes monetary, to actions performed on the site. The Experience Profile currently shows the goals for each Contact.

However, beyond the goal being registered we don’t know the context in which the goal was met. Wouldn’t it be great if we knew WHAT was added to the basket when the ‘Add to Basket’ goal was registered?

In my post extending xDB to show custom data I described how we can get that contextual data into xDB. I.e. a JavaScript function to initiate when an action is made by a User. Retrieving the contextual information from the markup of the page. Finally passing the goal and goal context to a MVC Controller to register it.

For example, a directory listing for a business’ employees. A goal is registered when the User clicks the ‘Call Now’ or ‘Email Now’ button to communicate with that member of staff. We will register the ‘Called Employee’ or ‘Emailed Employee’ goal but we can also include which employee contacted and the phone number or email address used.

But the Experience Profile doesn’t show this valuable contextual information even though its in xDB. So let’s do something about that.

Extending the Goals ExperienceProfileContactViews Pipeline

Back to the old ExperienceProfileContactViews Pipeline Groups. This time, we’re going to extend the Goals section to include two new pipelines. First, a pipeline to add an additional column to the DataTable to hold the Goal’s Context. Secondly, a pipeline to fill that column with data retrieved from xDB via a new DataSourceQuery, which we’ll get to shortly.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactViews">
<pipelines>
<goals>
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.AddGoalDescriptionColumn, JonathanRobbins.DisplayGoalContext" />
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.FillGoalDescription, JonathanRobbins.DisplayGoalContext" />
</goals>
</pipelines>
</group>
</pipelines>
</sitecore>

With me so far? Good.

Adding the column is as simple as below but needs to occur once the GoalsDataTable has been constructed by the appropriate pipeline.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class AddGoalDescriptionColumn : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
if (args.ResultTableForView == null)
return;
args.ResultTableForView.Columns.Add(Schemas.GoalDescription.ToColumn());
}
}

Entering the context of the goal into that column is a little more work. Pulling apart the data in the QueryResult to get the goal context and entering it into our new column. This needs to occur after the existing pipeline which populates the goals with data from xDB.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class FillGoalDescription : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
DataTable resultTableForView = args.ResultTableForView;
Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", new object[] { args.ReportParameters.ViewName });
int i = 0;
foreach (DataRow row in resultTableForView.AsEnumerable())
{
var goalData = args.QueryResult.Rows[i].ItemArray[4];
if (goalData != null)
{
row[Schemas.GoalDescription.Name] = goalData;
}
i++;
}
}
}

The schema which files both reference is as follows


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


namespace JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals
{
public static class Schemas
{
public static ViewField<string> GoalDescription = new ViewField<string>("GoalDescription");
}
}
view raw

Schemas.cs

hosted with ❤ by GitHub

Retrieving Goal information from xDB

As you may know, the Goal Type is a PageEvent. PageEvents are stored in xDB against a Contact’s Interaction, not the Contact themselves. Which makes sense architecturally speaking. A Contact interacts with the site, that interaction includes a number of page views, a page view may result in a page event.

When registering a goal I use the Description property to hold the context of the goal. In this example, it would be the name of the Employee the user chose to contact and medium of the contact; phone number or email address. This is done by the following;


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class PageEventController : Controller
{
[System.Web.Mvc.HttpPost]
public JsonResult RegisterGoal(string goalId, string goalDescription)
{
Item eventItem = Sitecore.Context.Database.GetItem(goalId);
var goal = new PageEventItem(eventItem);
if (!Tracker.IsActive)
Tracker.StartTracking();
Sitecore.Analytics.Model.PageEventData eventData = Tracker.Current.CurrentPage.Register(goal);
eventData.Data = goal["Description"] + " " + goalDescription;
Tracker.Current.Interaction.AcceptModifications();
return Json(new PageEventRequestResult()
{
Success = true,
Message = "Successfully registered goal",
});
}
}

This Goal Description ends up in the Data property on the PageEventData type which is written to xDB (so we don’t have to worry about writing something custom there). So in order to retrieve it for our new column we need to query xDB. To do that we need to replace the existing Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals type with our own.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactDataSourceQueries">
<pipelines>
<goalsquery>
<processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.ReportingServerDatasource.Goals.GetGoals, JonathanRobbins.DisplayGoalContext" />
</goalsquery>
</pipelines>
</group>
</pipelines>
</sitecore>

Similar to the existing GetGoals type we are replacing, we are querying the Interactions Collection in xDB for entries matching the Contact’s Id we are currently viewing in the Experience Profile. We are asserting that at least one PageEvent exists in the Page.PageEvents property so we don’t get null PageEvents. Our query requests which Fields we want from the entry specifying the Pages_PageEvents_Data property which holds our goal context.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class GetGoals : ReportProcessorBase
{
private readonly QueryBuilder goalsQueryBuilder = new QueryBuilder()
{
collectionName = "Interactions",
QueryParms =
{
{
"ContactId", "@contactId"
},
{
"Pages.PageEvents.0", "{$exists:1}"
}
},
Fields =
{
"_id",
"ContactId",
"Pages_PageEvents_PageEventDefinitionId",
"Pages_PageEvents_DateTime",
"Pages_PageEvents_Data",
"Pages_Url_Path",
"Pages_Url_QueryString",
"Pages_PageEvents_Value",
"Pages_Item__id"
}
};
public override void Process(ReportProcessorArgs args)
{
DataTable goalsData = this.GetGoalsData(args.ReportParameters.ContactId);
args.QueryResult = goalsData;
}
private DataTable GetGoalsData(Guid contactId)
{
ReportDataProvider reportDataProvider = this.GetReportDataProvider();
Assert.IsNotNull((object)reportDataProvider, "provider should not be null");
return reportDataProvider.GetData("collection", new ReportDataQuery(this.goalsQueryBuilder.Build())
{
Parameters =
{
{
"@contactId", (object) contactId
}
}
}, new CachingPolicy()
{
NoCache = true
}).GetDataTable();
}
}
view raw

GetGoals.cs

hosted with ❤ by GitHub

Almost there

The Experience Profile being built with SPEAK means to finish this off we need to do a bit. Fortunately, it is a very small bit. We simply need to add a new Item based on the ColumnField template in the Goals ListControl so that our new column displays in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals entering a sensible HeaderText and a DataField value that matches our schema name.

The result of our hard work;

Sitecore Goals Context

And that’s it!

A good effective improvement to the Goals area of the Sitecore Experience Profile. Give your Content Editors the what, why and how for when the User achieved the goal.

If you like this but don’t fancy doing work I’ll be submitting this as a Sitecore Marketplace Module shortly so you can just take it from there. I will update this post soon as its available for all!

Display the Context of a Sitecore Goal in the Experience Profile

$
0
0

For a while now I’ve been writing about collecting and displaying data unique to a client in Sitecore’s Experience Profile. What went hand in hand with that is the use of Sitecore Goals and Outcomes – giving a business value, sometimes monetary, to actions performed on the site. The Experience Profile currently shows the goals for each Contact.

However, beyond the goal being registered we don’t know the context in which the goal was met. Wouldn’t it be great if we knew WHAT was added to the basket when the ‘Add to Basket’ goal was registered?

In my post extending xDB to show custom data I described how we can get that contextual data into xDB. I.e. a JavaScript function to initiate when an action is made by a User. Retrieving the contextual information from the markup of the page. Finally passing the goal and goal context to a MVC Controller to register it.

For example, a directory listing for a business’ employees. A goal is registered when the User clicks the ‘Call Now’ or ‘Email Now’ button to communicate with that member of staff. We will register the ‘Called Employee’ or ‘Emailed Employee’ goal but we can also include which employee contacted and the phone number or email address used.

But the Experience Profile doesn’t show this valuable contextual information even though its in xDB. So let’s do something about that.

Extending the Goals ExperienceProfileContactViews Pipeline

Back to the old ExperienceProfileContactViews Pipeline Groups. This time, we’re going to extend the Goals section to include two new pipelines. First, a pipeline to add an additional column to the DataTable to hold the Goal’s Context. Secondly, a pipeline to fill that column with data retrieved from xDB via a new DataSourceQuery, which we’ll get to shortly.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactViews">
<pipelines>
<goals>
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.AddGoalDescriptionColumn, JonathanRobbins.DisplayGoalContext" />
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.FillGoalDescription, JonathanRobbins.DisplayGoalContext" />
</goals>
</pipelines>
</group>
</pipelines>
</sitecore>

With me so far? Good.

Adding the column is as simple as below but needs to occur once the GoalsDataTable has been constructed by the appropriate pipeline.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class AddGoalDescriptionColumn : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
if (args.ResultTableForView == null)
return;
args.ResultTableForView.Columns.Add(Schemas.GoalDescription.ToColumn());
}
}

Entering the context of the goal into that column is a little more work. Pulling apart the data in the QueryResult to get the goal context and entering it into our new column. This needs to occur after the existing pipeline which populates the goals with data from xDB.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class FillGoalDescription : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
DataTable resultTableForView = args.ResultTableForView;
Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", new object[] { args.ReportParameters.ViewName });
int i = 0;
foreach (DataRow row in resultTableForView.AsEnumerable())
{
var goalData = args.QueryResult.Rows[i].ItemArray[4];
if (goalData != null)
{
row[Schemas.GoalDescription.Name] = goalData;
}
i++;
}
}
}

The schema which files both reference is as follows


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


namespace JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals
{
public static class Schemas
{
public static ViewField<string> GoalDescription = new ViewField<string>("GoalDescription");
}
}
view raw

Schemas.cs

hosted with ❤ by GitHub

Retrieving Goal information from xDB

As you may know, the Goal Type is a PageEvent. PageEvents are stored in xDB against a Contact’s Interaction, not the Contact themselves. Which makes sense architecturally speaking. A Contact interacts with the site, that interaction includes a number of page views, a page view may result in a page event.

When registering a goal I use the Description property to hold the context of the goal. In this example, it would be the name of the Employee the user chose to contact and medium of the contact; phone number or email address. This is done by the following;


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class PageEventController : Controller
{
[System.Web.Mvc.HttpPost]
public JsonResult RegisterGoal(string goalId, string goalDescription)
{
Item eventItem = Sitecore.Context.Database.GetItem(goalId);
var goal = new PageEventItem(eventItem);
if (!Tracker.IsActive)
Tracker.StartTracking();
Sitecore.Analytics.Model.PageEventData eventData = Tracker.Current.CurrentPage.Register(goal);
eventData.Data = goal["Description"] + " " + goalDescription;
Tracker.Current.Interaction.AcceptModifications();
return Json(new PageEventRequestResult()
{
Success = true,
Message = "Successfully registered goal",
});
}
}

This Goal Description ends up in the Data property on the PageEventData type which is written to xDB (so we don’t have to worry about writing something custom there). So in order to retrieve it for our new column we need to query xDB. To do that we need to replace the existing Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals type with our own.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactDataSourceQueries">
<pipelines>
<goalsquery>
<processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.ReportingServerDatasource.Goals.GetGoals, JonathanRobbins.DisplayGoalContext" />
</goalsquery>
</pipelines>
</group>
</pipelines>
</sitecore>

Similar to the existing GetGoals type we are replacing, we are querying the Interactions Collection in xDB for entries matching the Contact’s Id we are currently viewing in the Experience Profile. We are asserting that at least one PageEvent exists in the Page.PageEvents property so we don’t get null PageEvents. Our query requests which Fields we want from the entry specifying the Pages_PageEvents_Data property which holds our goal context.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class GetGoals : ReportProcessorBase
{
private readonly QueryBuilder goalsQueryBuilder = new QueryBuilder()
{
collectionName = "Interactions",
QueryParms =
{
{
"ContactId", "@contactId"
},
{
"Pages.PageEvents.0", "{$exists:1}"
}
},
Fields =
{
"_id",
"ContactId",
"Pages_PageEvents_PageEventDefinitionId",
"Pages_PageEvents_DateTime",
"Pages_PageEvents_Data",
"Pages_Url_Path",
"Pages_Url_QueryString",
"Pages_PageEvents_Value",
"Pages_Item__id"
}
};
public override void Process(ReportProcessorArgs args)
{
DataTable goalsData = this.GetGoalsData(args.ReportParameters.ContactId);
args.QueryResult = goalsData;
}
private DataTable GetGoalsData(Guid contactId)
{
ReportDataProvider reportDataProvider = this.GetReportDataProvider();
Assert.IsNotNull((object)reportDataProvider, "provider should not be null");
return reportDataProvider.GetData("collection", new ReportDataQuery(this.goalsQueryBuilder.Build())
{
Parameters =
{
{
"@contactId", (object) contactId
}
}
}, new CachingPolicy()
{
NoCache = true
}).GetDataTable();
}
}
view raw

GetGoals.cs

hosted with ❤ by GitHub

Almost there

The Experience Profile being built with SPEAK means to finish this off we need to do a bit. Fortunately, it is a very small bit. We simply need to add a new Item based on the ColumnField template in the Goals ListControl so that our new column displays in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals entering a sensible HeaderText and a DataField value that matches our schema name.

The result of our hard work;

Sitecore Goals Context

And that’s it!

A good effective improvement to the Goals area of the Sitecore Experience Profile. Give your Content Editors the what, why and how for when the User achieved the goal.

If you like this but don’t fancy doing work I’ll be submitting this as a Sitecore Marketplace Module shortly so you can just take it from there. I will update this post soon as its available for all!


Display the Context of a Sitecore Goal in the Experience Profile

$
0
0

For a while now I’ve been writing about collecting and displaying data unique to a client in Sitecore’s Experience Profile. What went hand in hand with that is the use of Sitecore Goals and Outcomes – giving a business value, sometimes monetary, to actions performed on the site. The Experience Profile currently shows the goals for each Contact.

However, beyond the goal being registered we don’t know the context in which the goal was met. Wouldn’t it be great if we knew WHAT was added to the basket when the ‘Add to Basket’ goal was registered?

In my post extending xDB to show custom data I described how we can get that contextual data into xDB. I.e. a JavaScript function to initiate when an action is made by a User. Retrieving the contextual information from the markup of the page. Finally passing the goal and goal context to a MVC Controller to register it.

For example, a directory listing for a business’ employees. A goal is registered when the User clicks the ‘Call Now’ or ‘Email Now’ button to communicate with that member of staff. We will register the ‘Called Employee’ or ‘Emailed Employee’ goal but we can also include which employee contacted and the phone number or email address used.

But the Experience Profile doesn’t show this valuable contextual information even though its in xDB. So let’s do something about that.

Extending the Goals ExperienceProfileContactViews Pipeline

Back to the old ExperienceProfileContactViews Pipeline Groups. This time, we’re going to extend the Goals section to include two new pipelines. First, a pipeline to add an additional column to the DataTable to hold the Goal’s Context. Secondly, a pipeline to fill that column with data retrieved from xDB via a new DataSourceQuery, which we’ll get to shortly.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactViews">
<pipelines>
<goals>
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.AddGoalDescriptionColumn, JonathanRobbins.DisplayGoalContext" />
<processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals.Processors.FillGoalDescription, JonathanRobbins.DisplayGoalContext" />
</goals>
</pipelines>
</group>
</pipelines>
</sitecore>

With me so far? Good.

Adding the column is as simple as below but needs to occur once the GoalsDataTable has been constructed by the appropriate pipeline.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class AddGoalDescriptionColumn : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
if (args.ResultTableForView == null)
return;
args.ResultTableForView.Columns.Add(Schemas.GoalDescription.ToColumn());
}
}

Entering the context of the goal into that column is a little more work. Pulling apart the data in the QueryResult to get the goal context and entering it into our new column. This needs to occur after the existing pipeline which populates the goals with data from xDB.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class FillGoalDescription : ReportProcessorBase
{
public override void Process(ReportProcessorArgs args)
{
DataTable resultTableForView = args.ResultTableForView;
Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", new object[] { args.ReportParameters.ViewName });
int i = 0;
foreach (DataRow row in resultTableForView.AsEnumerable())
{
var goalData = args.QueryResult.Rows[i].ItemArray[4];
if (goalData != null)
{
row[Schemas.GoalDescription.Name] = goalData;
}
i++;
}
}
}

The schema which files both reference is as follows


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


namespace JonathanRobbins.DisplayGoalContext.Reporting.Contact.Goals
{
public static class Schemas
{
public static ViewField<string> GoalDescription = new ViewField<string>("GoalDescription");
}
}
view raw

Schemas.cs

hosted with ❤ by GitHub

Retrieving Goal information from xDB

As you may know, the Goal Type is a PageEvent. PageEvents are stored in xDB against a Contact’s Interaction, not the Contact themselves. Which makes sense architecturally speaking. A Contact interacts with the site, that interaction includes a number of page views, a page view may result in a page event.

When registering a goal I use the Description property to hold the context of the goal. In this example, it would be the name of the Employee the user chose to contact and medium of the contact; phone number or email address. This is done by the following;


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class PageEventController : Controller
{
[System.Web.Mvc.HttpPost]
public JsonResult RegisterGoal(string goalId, string goalDescription)
{
Item eventItem = Sitecore.Context.Database.GetItem(goalId);
var goal = new PageEventItem(eventItem);
if (!Tracker.IsActive)
Tracker.StartTracking();
Sitecore.Analytics.Model.PageEventData eventData = Tracker.Current.CurrentPage.Register(goal);
eventData.Data = goal["Description"] + " " + goalDescription;
Tracker.Current.Interaction.AcceptModifications();
return Json(new PageEventRequestResult()
{
Success = true,
Message = "Successfully registered goal",
});
}
}

This Goal Description ends up in the Data property on the PageEventData type which is written to xDB (so we don’t have to worry about writing something custom there). So in order to retrieve it for our new column we need to query xDB. To do that we need to replace the existing Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals type with our own.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


<sitecore>
<pipelines>
<group groupName="ExperienceProfileContactDataSourceQueries">
<pipelines>
<goalsquery>
<processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
type="JonathanRobbins.DisplayGoalContext.Reporting.ReportingServerDatasource.Goals.GetGoals, JonathanRobbins.DisplayGoalContext" />
</goalsquery>
</pipelines>
</group>
</pipelines>
</sitecore>

Similar to the existing GetGoals type we are replacing, we are querying the Interactions Collection in xDB for entries matching the Contact’s Id we are currently viewing in the Experience Profile. We are asserting that at least one PageEvent exists in the Page.PageEvents property so we don’t get null PageEvents. Our query requests which Fields we want from the entry specifying the Pages_PageEvents_Data property which holds our goal context.


This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


public class GetGoals : ReportProcessorBase
{
private readonly QueryBuilder goalsQueryBuilder = new QueryBuilder()
{
collectionName = "Interactions",
QueryParms =
{
{
"ContactId", "@contactId"
},
{
"Pages.PageEvents.0", "{$exists:1}"
}
},
Fields =
{
"_id",
"ContactId",
"Pages_PageEvents_PageEventDefinitionId",
"Pages_PageEvents_DateTime",
"Pages_PageEvents_Data",
"Pages_Url_Path",
"Pages_Url_QueryString",
"Pages_PageEvents_Value",
"Pages_Item__id"
}
};
public override void Process(ReportProcessorArgs args)
{
DataTable goalsData = this.GetGoalsData(args.ReportParameters.ContactId);
args.QueryResult = goalsData;
}
private DataTable GetGoalsData(Guid contactId)
{
ReportDataProvider reportDataProvider = this.GetReportDataProvider();
Assert.IsNotNull((object)reportDataProvider, "provider should not be null");
return reportDataProvider.GetData("collection", new ReportDataQuery(this.goalsQueryBuilder.Build())
{
Parameters =
{
{
"@contactId", (object) contactId
}
}
}, new CachingPolicy()
{
NoCache = true
}).GetDataTable();
}
}
view raw

GetGoals.cs

hosted with ❤ by GitHub

Almost there

The Experience Profile being built with SPEAK means to finish this off we need to do a bit. Fortunately, it is a very small bit. We simply need to add a new Item based on the ColumnField template in the Goals ListControl so that our new column displays in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals entering a sensible HeaderText and a DataField value that matches our schema name.

The result of our hard work;

Sitecore Goals Context

And that’s it!

A good effective improvement to the Goals area of the Sitecore Experience Profile. Give your Content Editors the what, why and how for when the User achieved the goal.

If you like this but don’t fancy doing work I’ll be submitting this as a Sitecore Marketplace Module shortly so you can just take it from there. I will update this post soon as its available for all!

Viewing all 29 articles
Browse latest View live