<?xml-stylesheet href="/pretty-feed-v2.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Trys Mudford's Blog</title>
    <link>https://www.trysmudford.com/tags/tracking/</link>
    <description>Posts, thoughts, links and photos from Trys</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 08 Dec 2023 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.trysmudford.com/blog/index.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title>Declarative Tracking</title>
      <link>https://www.trysmudford.com/blog/declarative-tracking/</link>
      <pubDate>Fri, 08 Dec 2023 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/declarative-tracking/</guid>
      <description><![CDATA[
<p>On &lsquo;reactive&rsquo; projects, it&rsquo;s pretty common to attach event tracking to buttons/forms etc using inline <code>onClick</code>/<code>onSubmit</code> handlers. It usually starts with the harmless &ldquo;can we track how many times this button is pressed&rdquo; request, and without thinking, you attach a handler that looks something like this: <code>onClick={() =&gt; ga('event'))</code>. This continues for many months &amp; years until your codebase is littered with these inline tracking functions.</p>
<p>I&rsquo;ve had the opportunity to work on a greenfield project recently and was able to trial an alternative approach. A more declarative approach. I&rsquo;m certainly not the first person to think of this, but I wanted to share my experience. It looks a bit like this:</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">button</span>
    <span class="na">type</span><span class="o">=</span><span class="s">&#34;button&#34;</span>
    <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;cta&#34;</span>
    <span class="na">data-track-category</span><span class="o">=</span><span class="s">&#34;homepage&#34;</span>
    <span class="na">data-track-name</span><span class="o">=</span><span class="s">&#34;heroCta&#34;</span>
<span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">form</span> <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;submit&#34;</span> <span class="na">data-track-category</span><span class="o">=</span><span class="s">&#34;createAccount&#34;</span><span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;...&#34;</span> <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;click&#34;</span><span class="p">&gt;</span>
</code></pre></div><p>Any anchor, form, or button can be marked up with <code>data-</code> attributes that describe the event. Rather than attach a handler to each event, we register one global <code>click</code> and <code>submit</code> event handler on the <code>document</code>. Thanks to event bubbling and <code>event.target.closest</code>, we can easily filter out elements that don&rsquo;t have the <code>data-track</code> attribute.</p>
<h2 id="benefits">Benefits</h2>
<ul>
<li>This is a substantially more efficient pattern to attaching events to each element, particularly in a re-render heavy framework like React</li>
<li>Abstracting this functionality allows us to declare less per element. For example, we can read <code>event.target.textContent</code>, rather than re-supply the label on every call-to-action button. We can automatically pull in the <code>href</code> on an anchor once, rather than duplicate the URL in an inline function</li>
<li>There&rsquo;s less code to write per component</li>
<li>We can add new tracking providers without touching every component</li>
<li>Common values can be set globally, rather than per component. For example, we may wish to pass the current page URL for all events</li>
<li>This all started in a Next.js project. I&rsquo;d painstakingly constructed my server/client components to minimise the number of client components. And that would&rsquo;ve been entirely obliterated with inline tracking functions, that have to exist in a <code>'use client'</code> file. This approach allowed me to keep as many components as pure server components</li>
</ul>
<h2 id="drawbacks">Drawbacks</h2>
<ul>
<li>It&rsquo;s extra HTML to send over the wire - sending markup for something that is entirely client-side isn&rsquo;t ideal</li>
<li>Everything that&rsquo;s tracked is easier to see in dev-tools - it&rsquo;s not as if inline functions are hidden, but they&rsquo;re certainly less obvious. This could be a positive; if you&rsquo;re not comfortable with someone seeing what&rsquo;s being tracked, then maybe it shouldn&rsquo;t be tracked? The tracking sunlight test</li>
<li>Non-primitives can&rsquo;t easily be tracked without stringifying JSON</li>
</ul>
<h2 id="abstracting-the-tracking">Abstracting the tracking</h2>
<p>Rather than calling, say, Google Analytics directly in the global handler, I&rsquo;ve taken to dispatching a <code>CustomEvent</code> in a consistent format that can be listened to by any number of handlers. For the few times you may need to track something outside of the global handler, you can dispatch a <code>CustomEvent</code> with the same format and it&rsquo;ll be fed to all the handlers.</p>
<h2 id="implementation">Implementation</h2>
<p>This is a rough implementation of the concept - I can&rsquo;t guarantee perfection but it&rsquo;s broadly what I&rsquo;ve been using:</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kr">const</span> <span class="nx">TRACKING_EVENT</span> <span class="o">=</span> <span class="s1">&#39;namespaced:tracking&#39;</span><span class="p">;</span>

<span class="c1">// Fire events to all tracking providers
</span><span class="c1"></span><span class="kr">const</span> <span class="nx">trackEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventAction</span><span class="p">,</span> <span class="nx">trackingDetail</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span>
        <span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="nx">TRACKING_EVENT</span><span class="p">,</span> <span class="p">{</span>
            <span class="nx">detail</span><span class="o">:</span> <span class="p">{</span>
                <span class="nx">eventAction</span><span class="p">,</span>
                <span class="nx">trackingDetail</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}),</span>
    <span class="p">);</span>
<span class="p">};</span>

<span class="kr">const</span> <span class="nx">globalTrackingHandler</span> <span class="o">=</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">event</span><span class="o">:</span> <span class="nx">Event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Detect if we&#39;re on a trackable element
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span> <span class="nx">as</span> <span class="nx">HTMLElement</span><span class="p">).</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;[data-track]&#39;</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">target</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="c1">// Prevent click events from firing on trackable forms
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">track</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">action</span> <span class="o">===</span> <span class="s1">&#39;submit&#39;</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;click&#39;</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="c1">// Extract data from the trackable element
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">category</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-category&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">label</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-label&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-name&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-name&#39;</span><span class="p">]</span> <span class="o">??</span> <span class="nx">target</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;href&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>

    <span class="c1">// Pass events to the global tracking helper
</span><span class="c1"></span>    <span class="k">switch</span> <span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="s1">&#39;click&#39;</span><span class="o">:</span> <span class="p">{</span>
            <span class="nx">trackEvent</span><span class="p">(</span><span class="nx">action</span><span class="p">,</span> <span class="p">{</span>
                <span class="nx">category</span><span class="p">,</span>
                <span class="nx">label</span><span class="p">,</span>
                <span class="nx">name</span><span class="p">,</span>
                <span class="nx">url</span><span class="p">,</span>
            <span class="p">});</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">case</span> <span class="s1">&#39;submit&#39;</span><span class="o">:</span> <span class="p">{</span>
            <span class="kr">const</span> <span class="nx">formLabel</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;[type=&#34;submit&#34;]&#39;</span><span class="p">)</span><span class="o">?</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">??</span> <span class="nx">label</span><span class="p">;</span>
            <span class="kr">const</span> <span class="nx">formUrl</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;action&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="nx">url</span><span class="p">;</span>
            <span class="nx">trackEvent</span><span class="p">(</span><span class="s1">&#39;cta&#39;</span><span class="p">,</span> <span class="p">{</span>
                <span class="nx">category</span><span class="p">,</span>
                <span class="nx">label</span><span class="o">:</span> <span class="nx">formLabel</span><span class="p">,</span>
                <span class="nx">name</span><span class="p">,</span>
                <span class="nx">url</span><span class="o">:</span> <span class="nx">formUrl</span><span class="p">,</span>
            <span class="p">});</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">default</span><span class="o">:</span>
            <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">globalTrackingHandler</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;submit&#39;</span><span class="p">,</span> <span class="nx">globalTrackingHandler</span><span class="p">);</span>

<span class="kr">const</span> <span class="nx">specificTrackingHandler</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="kr">const</span> <span class="p">{</span> <span class="nx">detail</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">detail</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="kr">const</span> <span class="p">{</span> <span class="nx">eventAction</span><span class="p">,</span> <span class="nx">trackingDetail</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">detail</span><span class="p">;</span>

    <span class="k">switch</span> <span class="p">(</span><span class="nx">eventAction</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="s1">&#39;click&#39;</span><span class="o">:</span>
            <span class="nx">trackClick</span><span class="p">(</span><span class="nx">trackingDetail</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="s1">&#39;cta&#39;</span><span class="o">:</span>
            <span class="nx">trackCta</span><span class="p">(</span><span class="nx">trackingDetail</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">default</span><span class="o">:</span>
            <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">TRACKING_EVENT</span><span class="p">,</span> <span class="nx">specificTrackingHandler</span><span class="p">);</span>
</code></pre></div>]]>
      </description>
    </item>
    
  </channel>
</rss>