View Sitecore xConnect logs in Log2Console

Few days ago I blogged about how to view Commerce logs in Log2Console.

I searched a little bit how to  view  xConnect logs in real time, but I didn’t find any article how to add a custom serilog sink.

I made a small research and here is there solution:

To view xConnect logs in Log2Console you need to follow next steps :

  1. Add Serilog.Sinks.UDP.dll and Serilog.Sinks.PeriodicBatching.dll to your xConnect bin folder . (https://github.com/FantasticFiasco/serilog-sinks-udp-sample-dotnet-framework)
  2.  Add a new Serilog configuration  into :     yourxConnectfolder\App_Data\Config\Sitecore\CoreServices and name it : sc.Serilog.Udp.xml where you set the remoteAddress and remotePort:

3. To view xconnect logs from AutomationEngine, IndexWorker and ProcessEngine you need to copy above file  to the CoreServices folder (example  for IndexWorker : c:\inetpub\wwwroot\xconnectFolder\App_Data\jobs\continuous\IndexWorker\App_Data\config\sitecore\CoreServices\ )

4. Check log2console (I already setup the receivers)

log2consoleXconnect.PNG

Sitecore Commerce logviewing using Log2Console

To view Sitecore logs in Log2Console is pretty easy. Sitecore  Platform is using log4net and you can add an UdpAppender for logging in an external application.

On few blogs is described how to do it:

  1. http://blog.jan.hebnes.dk/2015/12/hackthedot-sitecore-logviewing-made.html
  2. https://community.sitecore.net/developers/f/8/t/136 ( few people described different tools for sitecore logs)
  3. https://publications.soulcode.agency/you-dont-monitor-your-local-sitecore-instance-close-enough/

Sitecore Commerce is a .NET Core application, and is using Serilog for log messages.

We will use an external nuget package to send UDP package over the network: https://github.com/FantasticFiasco/serilog-sinks-udp

To view the commerce logs in Log2Console we need to follow next steps:

  1. Add a settings in commerce config.json to enable/disable sending UDP packages over the network.

writetoudp2.JPG

2. Add reference to Serilog.Sinks.Udp nuget package

nugetpackage.JPG

3. Modify commerce engine to use Serilog.Sink.Udp

The UDP packages are sent on port 7071.

At the end of Startup.cs add next code:

var writeToUdp = false;

//check if writotoUdp is enabled
if (bool.TryParse(Configuration.GetSection(“Serilog:WritetoUDP”).Value, out writeToUdp))
{
if (writeToUdp)
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink((ILogEventSink)Log.Logger)
.WriteTo.Udp(“localhost”, 7071, System.Net.Sockets.AddressFamily.InterNetwork)
.CreateLogger();
}

4. Add a log2console receiver, it will work on port 7071.

log2consoleReceiver.JPG

and commerce logs will appear on Log2console

log2consoleLogs.JPG

Extend Commerce Entity Views

To extend commerce entity view you need to have a look on Commerce Views service

The Commerce Views service is provided by the Sitecore.Commerce.Plugin.Views
plugin.

Commerce views are also designed to feed directly into a dynamic data-driven business tool experience. You can activate, extend, or remove functionality in the business tools through custom-developed plugins, without having to modify the out-of-box plugins or modify the source code of the business tools themselves. This approach dramatically improves upgradability.

In the Sitecore Commerce architecture, this view layer is provided on the server side, instead of the client side.In this way you don’t need to have frontend knowledge. This allows standard Commerce plugins to extend existing views and add new views.

Commerce views are exposed via the Authoring API. This Authoring API is based on OData.

The EntityView is the core Commerce artifact that supports views.

An EntityView represents a flattened, dynamic service response focused on supporting a dynamic user experience.

The EntityView provides a business user experience that is completely customizable and extensible. The EntityView allows the business user to take actions and dynamically updates the view without requiring significant intelligence or customization of the user experience itself.

The EntityView is a simple POCO class that provides a property bag. The EntityView can have child EntityViews. You can build out more complex views as needed.

A Composite EntityView includes a master view and child views.

The master view represents the overall page itself. It normally does not contain properties, but could. The master view contains a list of child views that each represent a section of the user interface. It also contains a list of actions that can be executed against the entity. An example of a child view is the Details section of a page. For example, on an Order page, the child view is the section that presents the order confirmation identifier, the date, and so on.

In the next example you will find how to add to sellable items  the first image from product “Images” field.

 

 

Add a policy where you keep the value of the CM server.

Add the policy to the PlugIn.Habitat.CommerceAuthoring-1.0.0.json .

Inject the pipeline block which extend the entitiy view after PopulateEntityViewActionsBlock :

Implement the PopulateSellableItemImages pipelineblock:

 

 

 

 

 

 

Commerce Computed Indexfield

Computed fields allow you to add entirely new fields in your search indexes, in addition to those of Sitecore.

To create a computed index field for commerce entities you need to follow next steps:

  1. Create your own class which inherits from : Sitecore.Commerce.Engine.Connect.Search.ComputedFields.BaseCommerceComputedField

2. Customize the class to meet your particular needs.

The “JToken entity” object contains few information about your sellableitem (in the below example is a sellableitem from SXA)

3. Add the Computed Field in the Configuration:

 

 

Sitecore Commerce Icons

Ten years ago when I started working with Sitecore, one of the first thing I learn it was how to select icons for items templates.

In Sitecore Commerce is a bit different how to choose icons for entity views.You don’t have a picker for icons.

Icons in Sitecore Commerce are stored in Bizfx tools in the root folder in SitecoreIcons.*.svg file.

bizfxtool

To view all the icons from the Bizfx toool you can use  http://fontello.com/

You can drag and drop and select the icons .

fontello

 

After you select the SVG file you will see icons available for Sitecore Commerce.

BizfxIcons

Using Fontello you can search for icons :

calendarclock

To create an entity view with a selected icons you have use :

var newEntityView = new EntityView
{
Name = “CustomEntityView”,
DisplayName = “CustomEntityView”,
Icon = “calendar_clock”,
ItemId = “CustomEntityViewId”
};

 

The result for on Commerce Dashboard is :

CustomEntityView

In the next blog I will explain how to add a new EntityView to CommerceDashboard

 

Field Validators on Experience Editor

On Sitecore 8 I added few fields validators.
Everything works perfectly on Content Editors but not on Experience Editor.

On config files we have next setting:


<!--
  WEB EDIT ENABLE VALIDATION
            If true, the Page Editor will execute item and field validation rules whenever a user tries to save items in the Page Editor.
            Only 'Critical' and 'Fatal' validators are evaluated, and item validation rules are executed for the current context item only.
            Field validation rules are only executed for fields that the current user can modify in the Page Editor.
            Default value: true
      
-->
<setting name="WebEdit.EnableValidation" value="true" patch:source="Sitecore.ExperienceEditor.config"/> 
 

but is ignored on Experience Editor.

I found a new solution switching Experience Editor from Speak UI to Sheer UI.
How to switch it :

Modify on App_Config\Include\Sitecore.ExperienceEditor.config
mvc.renderPageExtenders pipeline

1. Comment out the following line in the:

<pageextender type="Sitecore.ExperienceEditor.Speak.Ribbon.PageExtender.RibbonPageExtender, Sitecore.ExperienceEditor.Speak.Ribbon" />
[/soucecode]
2. Uncomment the following lines:
 
<pageextender type="Sitecore.Layouts.PageExtenders.PreviewPageExtender, Sitecore.ExperienceEditor" />
<pageextender type="Sitecore.Layouts.PageExtenders.WebEditPageExtender, Sitecore.ExperienceEditor" />
<pageextender type="Sitecore.Layouts.PageExtenders.DebuggerPageExtender, Sitecore.ExperienceEditor" />
 

If you have a Mvc Solution you need to change Sitecore.MvcExperienceEditor.config
1. Uncomment the page extenders below and comment the “SPEAK-based” Experience Editor ribbon processors to switch to old SheerUI-based Experience Editor ribbon.

 
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderPageEditorExtender, Sitecore.Mvc.ExperienceEditor"></processor>
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderPreviewExtender, Sitecore.Mvc.ExperienceEditor"></processor>
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderDebugExtender, Sitecore.Mvc.ExperienceEditor"></processor>
    

2. Comment next pipeline

   <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.SpeakRibbon.RenderPageEditorSpeakExtender, Sitecore.Mvc.ExperienceEditor"></processor>
   

Goals in a multisite solution

Yesterday it was a question on Community how to implement Goals in a multisite solution: https://community.sitecore.net/developers/f/9/t/1913

And if I write few blog posts about multisite I said to write also a blogpost about implementing goals in a multisite solution.

My site structure is :

Countries

My goals structure is :

Step 1 :

Modify site definition like. I added a new property to sites “goalsFolder”

  <site name="countrythree" patch:before="site[@name='website']" hostName="countrythree.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countrythree" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="en" goalsFolder="countrythree" />

  <site name="countrytwo" patch:before="site[@name='website']" hostName="countrytwo.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countrytwo" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="en" goalsFolder="countrytwo" />


  <site name="countryone" patch:before="site[@name='website']" hostName="countryone.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countryone" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="de-de" goalsFolder="countrytwo" />

  <site name="sitecoremvc-base" patch:before="site[@name='website']" hostName="*" enableTracking="true" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="50MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="25MB" filteredItemsCacheSize="10MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" cacheRenderingParameters="true" renderingParametersCacheSize="10MB" />
  

Step2:
Change type of command with name “anylitics:opengoals” from Sitecore.Analytics.config
into:

  <command name="analytics:opengoals" type="Sitecore.Analytics.OpenGoals,SitecoreDemo" patch:source="Sitecore.Analytics.config"/>

Step3:
Create a new class OpenGoals:

    using Sitecore;
    using System;
    namespace SitecoreDemo.Analytics
    {
      [Serializable, UsedImplicitly]
      public class OpenGoals : OpenTrackingField
      {
        // Methods
        protected override string GetUrl()
        {
            return "/sitecore/shell/~/xaml/Sitecore.Shell.Applications.Analytics.TrackingField.Goals.aspx";
        }
       }
    }
    

4. Create a new xaml file name it Goals.xaml and add it to folder: /Sitecore/Shell/Override

      <?xml version="1.0" encoding="UTF-8" ?>
<xamlControls xmlns:x="http://www.sitecore.net/xaml" xmlns:ajax="http://www.sitecore.net/ajax" xmlns:rest="http://www.sitecore.net/rest" xmlns:r="http://www.sitecore.net/renderings" xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols" xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense" xmlns:asp="http://www.sitecore.net/microsoft/webcontrols" xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <Sitecore.Shell.Applications.Analytics.TrackingField.Goals x:inherits="SitecoreDemo.Analytics.GoalsPage,SitecoreDemo">

    <Sitecore.Controls.DialogPage runat="server" Header="Goals" Text="Select the goals that you want to associate with the selected item.">
      <AjaxScriptManager runat="server"/>
      <ContinuationManager runat="server" />
      <Script runat="server" Src="/sitecore/Shell/Applications/Analytics/TrackingField/TrackingField.js" />


<Style runat="server">
        #GoalsList table tr > td {
          padding: 0 0 10px 0;
        }
      </Style>




<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0">


<tr>


<td height="100%">
            <GridPanel Width="100%" Height="100%" runat="server" Background="White">

              <Border runat="server" GridPanel.Style="height:100%" Height="100%">
                <Scrollbox id="GoalsList" runat="server" Height="100%" Padding="0px"/>
              </Border>
            </GridPanel>
          </td>


        </tr>


      </table>


        
        
    </Sitecore.Controls.DialogPage>
  </Sitecore.Shell.Applications.Analytics.TrackingField.Goals>


</xamlControls>

    

5. Create a new class GoalsPage

using Sitecore;
using Sitecore.Analytics;
using Sitecore.Analytics.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Extensions.XElementExtensions;
using Sitecore.Shell.Applications.Analytics.TrackingField;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using System.Xml.Linq;
using SitecoreDemo.Util;

namespace SitecoreDemo.Analytics
{
   [UsedImplicitly]
public class GoalsPage : TrackingFieldPageBase
{
    // Methods
    protected override void OK_Click()
    {
        Packet packet = new Packet("tracking", new string[0]);
        base.AddIgnoreFlag(packet);
        base.AddExistingProfiles(packet);
        base.AddExistingCampaigns(packet);
        if (HttpContext.Current != null)
        {
            List<PageEventItem> pageEventDefinitions = new List<PageEventItem>(from item in Tracker.DefinitionItems.PageEvents.Concat<PageEventItem>(Tracker.DefinitionItems.Goals)
                where item.IsDeployed
                select item);
            base.AddExistingNoneGoals(packet, pageEventDefinitions);
            CheckBoxList checkBoxList = this.GoalsList.Controls[0] as CheckBoxList;
            if (checkBoxList != null)
            {
                TrackingFieldPageBase.GetEvents(packet, pageEventDefinitions, checkBoxList);
            }
            SheerResponse.SetDialogValue(packet.ToString());
            base.OK_Click();
        }
    }

    protected override void OnLoad(EventArgs e)
    {
        Assert.ArgumentNotNull(e, "e");
        Assert.CanRunApplication("Content Editor/Ribbons/Chunks/Analytics - Attributes/Goals");
        base.OnLoad(e);
    }

    protected override void Render(XDocument doc)
    {
        Assert.ArgumentNotNull(doc, "doc");
        this.RenderGoals(doc);
    }

    private void RenderGoals(XDocument doc)
    {
        Assert.ArgumentNotNull(doc, "doc");
        List<string> selected = new List<string>();
        foreach (XElement element in doc.Descendants("event"))
        {
            selected.Add(element.GetAttributeValue("name"));
        }
        CheckBoxList child = new CheckBoxList {
            ID = "GoalsCheckBoxList"
        };
        this.GoalsList.Controls.Add(child);
        System.Web.UI.Page page = TrackingFieldPageBase.GetPage();

        var site = ItemUtil.GetSiteContextForItem(ItemUtil.GetItem(Sitecore.Context.Request.QueryString["id"]));
        var goalsFolder = site.Properties["goalsFolder"];
        if ((page != null) && !page.IsPostBack)
        {
            IOrderedEnumerable<PageEventItem> query;
            if (!string.IsNullOrEmpty(goalsFolder) && Sitecore.Context.ContentDatabase.GetItem(Tracker.DefinitionItems.Goals.Path+"/"+goalsFolder)!=null)
            {
                query = from e in Tracker.DefinitionItems.AllPageEvents
                        where e.InnerItem.Parent.Key.Equals(goalsFolder, StringComparison.InvariantCultureIgnoreCase) && (e.IsDeployed && e.IsGoal) && !e.IsSystem
                        orderby e.DisplayName
                        select e;
            }
            else
            {
                query = from e in Tracker.DefinitionItems.AllPageEvents
                        where(e.IsDeployed && e.IsGoal) && !e.IsSystem
                        orderby e.DisplayName
                        select e;
            }
            TrackingFieldPageBase.RenderCheckBoxList(child, query, selected);
        }
    }

    


    // Properties
    [UsedImplicitly]
    protected Scrollbox GoalsList { get; set; }
}
}

Step6. Create a new class OpenTrackingField

    using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;
using Sitecore.Web.UI.WebControls;
using System;
using Sitecore.StringExtensions;

namespace SitecoreDemo.Analytics
{
    [Serializable, UsedImplicitly]
    public class OpenTrackingField : Sitecore.Shell.Applications.Analytics.TrackingField.OpenTrackingField
    {
        CommandContext context;
        // Methods
        public override void Execute(CommandContext context)
        {
            this.context = context;
            base.Execute(context);
        }

        protected virtual string GetUrl()
        {
            return "/sitecore/shell/~/xaml/Sitecore.Shell.Applications.Analytics.TrackingField.aspx";
        }

        [UsedImplicitly]
        protected void Run(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Item item = base.DeserializeItems(args.Parameters["items"])[0];
            if (SheerResponse.CheckModified())
            {
                string str = args.Parameters["fieldid"];
                if (string.IsNullOrEmpty(str))
                {
                    str = "__Tracking";
                }
                if (args.IsPostBack)
                {
                    if (args.HasResult)
                    {
                        using (new StatisticDisabler(StatisticDisablerState.ForItemsWithoutVersionOnly))
                        {
                            item.Editing.BeginEdit();
                            item[str] = args.Result;
                            item.Editing.EndEdit();
                        }
                        if (AjaxScriptManager.Current != null)
                        {
                            AjaxScriptManager.Current.Dispatch("analytics:trackingchanged");
                        }
                        else
                        {
                            Context.ClientPage.SendMessage(this, "analytics:trackingchanged");
                            Context.ClientPage.SendMessage(this, "item:refresh(id={0})".FormatWith(new object[] { item.ID.ToString() }));
                        }
                    }
                }
                else if (item.Appearance.ReadOnly)
                {
                    SheerResponse.Alert("You cannot edit the '{0}' item because it is protected.", new string[] { item.DisplayName });
                }
                else if (!item.Access.CanWrite())
                {
                    SheerResponse.Alert("You cannot edit this item because you do not have write access to it.", new string[0]);
                }
                else
                {
                    UrlString urlString = new UrlString(this.GetUrl());
                    urlString.Add("id", context.Items[0].ID.ToShortID().ToString());
                    UrlHandle handle = new UrlHandle();
                    handle["tracking"] = item[str];
                    handle.Add(urlString);
                    this.ShowDialog(urlString.ToString());
                    args.WaitForPostBack();
                }
            }
        }
    }
}
     

Step7.
I am on countryone site and I want to add a new goal to a page.
Is showing me just the goals from countryone folder.

OpenGoalCountry

This code was tested on Sitecore 8.1.