<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Dan Copping - Medium</title> <link>https://world.optimizely.com/blogs/dan-copping---medium/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Search &amp; Navigation - Click Tracking</title>            <link>https://world.optimizely.com/blogs/dan-copping---medium/dates/2025/7/search--navigation---click-tracking/</link>            <description>&lt;p&gt;If you are implementing Search &amp;amp; Navigation (a.k.a. FIND) but cannot use the &amp;lsquo;Unified Search&amp;rsquo;, you will need to implement your own server-side tracking. There&amp;rsquo;s a number of blogs out there that touch on the subject but many are outdated and none seem to capture 100% of the requirements. &lt;br /&gt;&lt;br /&gt;If you don&amp;rsquo;t get everything correct, it&amp;rsquo;s likely you&amp;rsquo;ll be seeing all zeros for &amp;lsquo;Click-through rate&amp;rsquo; in the CMS:&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;border-style: solid;&quot; src=&quot;/link/70d352ee3bab485394ff014bae96d4f9.aspx&quot; alt=&quot;&quot; width=&quot;1056&quot; height=&quot;740&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The following will help anyone that needs to cater for:&lt;/p&gt;
&lt;ul class=&quot;ak-ul&quot;&gt;
&lt;li&gt;
&lt;p&gt;Multisite setups&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accurate reporting on &amp;lsquo;Most frequent searches&amp;rsquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accurate reporting on &amp;lsquo;Searches without hits&amp;rsquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accurate reporting on &amp;lsquo;Searches without relevant hits&amp;rsquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Accurate reporting &amp;lsquo;&lt;em&gt;People who searched for &#39;&lt;strong&gt;foo&lt;/strong&gt;&#39; also searched for&amp;hellip;&lt;/em&gt;&amp;rsquo;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note, I understand that Search &amp;amp; Navigation is kinda&#39; &lt;em&gt;on the way out &lt;/em&gt;(in favor of Graph) but hopefully this will still be of some use.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;This approach:&lt;/p&gt;
&lt;ol class=&quot;ak-ol&quot;&gt;
&lt;li&gt;
&lt;p&gt;Captures and submits the search query&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fires a &amp;lsquo;track event&#39; for the query&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Appends the tracking info (from the track event) to each search result URL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When a search result is clicked, this fires an event to a tracking route &amp;ndash; /go .&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tracking info is read from the query parameters&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tracking event is fired for the click event passing the query parameters&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The request is redirected to the search result page&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Get the Search Result&lt;/h2&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var searchResult = await ServiceLocator.Current.GetInstance&amp;lt;IClient&amp;gt;()
            .Search&amp;lt;T&amp;gt;(&amp;ldquo;en&amp;rdquo;)
            .For(&amp;ldquo;foo&amp;rdquo;)    
            .Take(20)
            .GetResultAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Track the Query&lt;/h2&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var tags = _tagsHelper.GetTags(false).ToList();
var trackResult = await ServiceLocator.Current.GetInstance&amp;lt;IClient&amp;gt;().Statistics().TrackQueryAsync(query.Query.ToLower(), c =&amp;gt;
                {
                    c.Id = new TrackContext().Id;
                    c.Query.Hits = searchResult.TotalMatching;
                    // c.Tags = _tagsHelper.GetTags(false).ToList();   -- don&#39;t do this here!
                    c.Tags = tags;
                }));&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Things to note:&lt;/h3&gt;
&lt;ul class=&quot;ak-ul&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;tags &lt;/strong&gt;will capture the current site, language and categories&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do not evaluate the tags within the command action of TrackQueryAsync. This is a problem in multi-site instances. Somehow, when you evaluate the tags from within the command action, the site id is always resolved to the website with the wildcard domain. Consider this if you have multiple sites sharing the same search code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a class=&quot;_mizu1p6i _1ah31bk5 _ra3xnqa1 _128m1bk5 _1cvmnqa1 _4davt94y _4bfu18uv _1hms8stv _ajmmnqa1 _vchhusvi _syaz14q2 _ect41gqc _1a3b18uv _4fpr8stv _5goinqa1 _f8pj14q2 _9oik18uv _1bnxglyw _jf4cnqa1 _30l314q2 _1nrm18uv _c2waglyw _1iohnqa1 _9h8h16c2 _1053w7te _1ienw7te _n0fxw7te _1vhvg3x0&quot; title=&quot;http://c.Id&quot; href=&quot;http://c.id/&quot;&gt;c.Id&lt;/a&gt; = new TrackContext().Id; &amp;ndash; this is used to set an id for the user &amp;ndash; it&amp;rsquo;s needed to determine what similar users also searched for and what they clicked on &amp;ndash; this can be exposed via _searchClient.Statistics()?.GetDidYouMeanAsync()&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Add Tracking Info to the Result URLs&lt;/h2&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var trackedUrls = new List&amp;lt;string&amp;gt;();
var resultList = searchResult.Hits.ToList();
for (int x = 0; x &amp;lt; resultList.Count; x ++)
{
    var trackedUrl = $&quot;https://www.mysite.com/&quot; +
                     $&quot;?query={System.Web.HttpUtility.UrlEncode(query)}&quot; +
                     $&quot;&amp;amp;trackid={trackResult.TrackId}&quot; +
                     $&quot;&amp;amp;hitid={resultList[x].Id}&quot; +
                     $&quot;&amp;amp;hittype={resultList[x].Type}&quot; +
                     $&quot;&amp;amp;trackuuid={trackResult.TrackUUId}&quot; +
                     $&quot;&amp;amp;trackhitpos={x +1}&quot; +
                     $&quot;&amp;amp;page=&quot;{resultList[x].Document.LinkURL};
    trackedUrls.Add(trackedUrl);   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a class=&quot;_mizu1p6i _1ah31bk5 _ra3xnqa1 _128m1bk5 _1cvmnqa1 _4davt94y _4bfu18uv _1hms8stv _ajmmnqa1 _vchhusvi _syaz14q2 _ect41gqc _1a3b18uv _4fpr8stv _5goinqa1 _f8pj14q2 _9oik18uv _1bnxglyw _jf4cnqa1 _30l314q2 _1nrm18uv _c2waglyw _1iohnqa1 _9h8h16c2 _1053w7te _1ienw7te _n0fxw7te _1vhvg3x0&quot; title=&quot;https://www.my-site.com/search/go/?query=my+query&amp;amp;trackId=yN8AqDDTSJEuof_p2uWVmg==&amp;amp;hitId=_507f0cdd-71b6-41ac-80ee-88bd90ddbac1_en&amp;amp;hitType=MySite_Pages_StandardPage&amp;amp;trackUuId=anbJ4Es_RGS_Kab0PANDeA&amp;amp;trackHitPos=5&amp;amp;page=/blog/article-one&quot; href=&quot;https://www.my-site.com/search/go/?query=my+query&amp;amp;trackId=yN8AqDDTSJEuof_p2uWVmg==&amp;amp;hitId=_507f0cdd-71b6-41ac-80ee-88bd90ddbac1_en&amp;amp;hitType=MySite_Pages_StandardPage&amp;amp;trackUuId=anbJ4Es_RGS_Kab0PANDeA&amp;amp;trackHitPos=5&amp;amp;page=/blog/article-one&quot;&gt;https://www.my-site.com/search/go/?query=my+query&amp;amp;trackid=yN8AqDDTSJEuof_p2uWVmg==&amp;amp;hitid=_507f0cdd-71b6-41ac-80ee-88bd90ddbac1_en&amp;amp;hittype=MySite_Pages_StandardPage&amp;amp;trackuuid=anbJ4Es_RGS_Kab0PANDeA&amp;amp;trackhitpos=5&amp;amp;page=/blog/article-one&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Things to Note:&lt;/h4&gt;
&lt;ul class=&quot;ak-ul&quot;&gt;
&lt;li&gt;
&lt;p&gt;URL Encode the search term when adding to the query parameters&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Track Clicks / Hits&lt;/h2&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  [HttpGet]
  [Route(&quot;go&quot;)]
  public async Task&amp;lt;RedirectResult&amp;gt; Go(
      [FromQuery] string query,
      [FromQuery] string trackid,
      [FromQuery] string hitid,
      [FromQuery] string hittype,
      [FromQuery] string trackuuid,
      [FromQuery] string trackhitpos,
      [FromQuery] string page)
  {
      Task.Run(async () =&amp;gt;
      {
          var locator = ServiceLocator.Current;
          var hitIdFormatted = $&quot;{hittype}/{hitid}&quot;;
          var tags = locator.GetInstance&amp;lt;IStatisticTagsHelper&amp;gt;().GetTags(false).ToList();
      
          await locator.GetInstance&amp;lt;IClient&amp;gt;().Statistics().TrackHitAsync(
              queryString: query,
              hitId: hitIdFormatted,
              command =&amp;gt;
              {
                  command.Id = trackid;
                  command.Hit.Id = hitIdFormatted;
                  command.Tags = tags;
                  command.Hit.QueryString = System.Web.HttpUtility.UrlDecode(query ?? string.Empty);
                  command.Hit.Position = int.TryParse(trackhitpos ?? &quot;0&quot;, out var pos) ? pos : 0;
                  command.AdditionalParameters = new AttributeDictionary() { { Uuid, trackuuid } };
              });
      });
    
      return this.Redirect(page);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Things to note:&lt;/h4&gt;
&lt;ul class=&quot;ak-ul&quot;&gt;
&lt;li&gt;
&lt;p&gt;By utilizing &amp;lsquo;Task.Run&amp;rsquo; here we are implementing a &amp;lsquo;fire-and-forget&#39; strategy; we do not wait for the tracking to succeed before redirecting the user &amp;ndash; this improves the performance / user-experience. Other/better ways to tackle this issue could be via a background service or queue. Some error handling and logging would also be a good idea.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;With all this in place, you should now see the correct statistics starting to roll-in.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;border-style: solid;&quot; src=&quot;/link/65182a3540304c0297aafaa20c46bb0f.aspx&quot; alt=&quot;image-20250721-060906.png&quot; width=&quot;1073&quot; height=&quot;728&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Gotchas:&lt;/h2&gt;
&lt;ul class=&quot;ak-ul&quot;&gt;
&lt;li&gt;
&lt;p&gt;Trim and lowercase the search term to prevent tracking different queries and clicks for effectively the same thing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Chromium-based browsers may prerender links on your page via the Speculation Rules API. This may lead to click tracking being fired even if the user does not click a result &amp;ndash; suggest you disable this on your search results page.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bots may also crawl your page and register searches without clicks. One way around this could be to only track requests where the referrer domain is your own site.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Users who click &amp;lsquo;back&amp;rsquo; via the browser will trigger a second query, and skew the result &amp;ndash; to get around this you can cache the result for the query based on a unique key for the request and user - &lt;br /&gt;var cacheKey = new TrackContext().Id + Request.QueryString + Request.Host;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/dan-copping---medium/dates/2025/7/search--navigation---click-tracking/</guid>            <pubDate>Mon, 21 Jul 2025 06:50:31 GMT</pubDate>           <category>Blog post</category></item><item> <title>Measuring Personalization — Optimizely CMS &amp; ODP</title>            <link>https://medium.com/p/b85fb7893fc2</link>            <description></description>            <guid>https://medium.com/p/b85fb7893fc2</guid>            <pubDate>Mon, 07 Apr 2025 04:32:15 GMT</pubDate>           <category>Blog post</category></item><item> <title>Anonymous Tracking Across Devices with Optimizely ODP</title>            <link>https://www.orchard.com.au/?p=1526</link>            <description>An article by Lead Integration Developer, Daniel Copping In this article, I’ll describe how you can use the Optimizely Data Platform (ODP) to effectively track anonymous user interactions between different sessions and devices. The key emphasis here is on&amp;#160;anonymous&amp;#160;users. While ODP makes it easy to stitch your customer records using their email address, if you [&amp;#8230;]</description>            <guid>https://www.orchard.com.au/?p=1526</guid>            <pubDate>Tue, 30 Apr 2024 03:03:06 GMT</pubDate>           <category>Blog post</category></item><item> <title>Real Time Personalization in Optimizely CMS with ODP</title>            <link>https://medium.com/p/2247ccf2d29f</link>            <description></description>            <guid>https://medium.com/p/2247ccf2d29f</guid>            <pubDate>Wed, 09 Aug 2023 09:34:12 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>