Skip to content

Instantly share code, notes, and snippets.

@rsukale
Last active February 11, 2019 01:25
Show Gist options
  • Save rsukale/f3341db7c382fda5d88105900d0fc2c2 to your computer and use it in GitHub Desktop.
Save rsukale/f3341db7c382fda5d88105900d0fc2c2 to your computer and use it in GitHub Desktop.

There are 2 approaches we can take to support Analytics in TDS.

  1. TDS components have no idea of that there is any analytics going on.
  2. TDS components are capable of emitting analytics events to component, given a well known named hook (i.e. a prop).

Both these approaches have pros and cons. Which I will briefly discuss below, along with code samples. Please take some time to think about these approaches. There's no TLDR! Also, its a long writeup. Please forgive the typos and let me know so I can fix them. I am hoping we can get on a call and discuss this proposal.

Approach 1: Fully agnostic TDS components

In this case, any TDS component does not do anything at all. And each client will have to, on an individual bases, determine which elements need an onClick etc, etc.

e.g

render() {
	<div>
	  <MobileNavigation
	onNavItemClick={track.nav.onNavItemClick}
			onAccordionClick={track.nav.onAccordionClick} 
			items={navItems}		
	   />
		 <DesktopNavigation
			 onNavItemClick={track.nav.onNavItemClick}
			 onNavItemHover={track.nav.onNavItemHover}
			 onUserInfoDropdownClick={tracker.nav.onUserInfoDropdownClick}
			 items={navItems}
		 />
	</div>
}

// -- trailhead-web/trailmaker etc, each client must do something like this if they wish to track DesktopNavigation and MobileNavigation ---
function onNavItemClick(e) {
  googleAnalytics.sendEvent('navEventClick', e.target.value);
}
function onNavItemClick(e) {
  googleAnalytics.sendEvent(e.target.value);
}
function onUserInfoDropdownClick() { ...// Business as usual }

Concerns with this approach

  • The best thing about this approach is that TDS knows nothing about analytics - one can almost incorrectly assume. Because if you look at the props above, now all of a sudden your TDS components have several new props onUserInfoDropdownClick or onAccordionClick onNavItemHover onNavItemClickwhich are internal implementations details of a component, that are now leaking outside just because we may need to track them. These props are in addition to any click events that were already on the links etc. They are valid user interactions on the UI if you want to answer questions like - Did people open the top level dropdown in mobile? Is the menu hierarchy on mobile confusing?
  • The other problem is - we have completely offloaded the responsibility to the client to figure out the correct analytics wrappers(or HOC) for each and every TDS component whenever it is being used. This can make it a pain to maintain with so many props and functions that we need to remember to pass to TDS components.
  • There is no concept of a context in which an element is rendered. You cannot tell if the DesktopHeader was rendered on the searchPage or on the Home Page, unless the client does all the heavy lifting of figuring it out, independently for every click, hover event.
  • If you notice above, the DesktopHeader and the MobileHeader both render the same data - navItems. However, the events they emit vary depending on the UI implementation of the component. In order to track different UI events, a client has to be aware of the implementation in order to pass the correct props.
  • The above example uses a fictitious library track.nav that handles tracking for a navigation items. This library will gradually increase in size as we a client wants to track more and more items. More code, i.e. more work to maintain.

In the light of all all of this, the proposal is of a second approach (as discussed below).

Approach 2: Analytics capable TDS components

This approach is based on the paradigm that every User Interface is a function of the data is it given, and that different components and use the same data but render complete different User interfaces.

This is a critical paradigm because the implementation of a component determines what kind of interactions a user can have with it.

This proposal only make the component capable of sending analytics event. But not actualy send them. That implementation is left upto the consumer.

Approach requires the following changes

  • To know that interactions can emit analytics events (if the client provides a well known hook/prop)
  • To send minimal data to the hook/prop.
  • The client is fully responsible for transmitting and setting up the hierarchy/context for the analytics event.

The implementation proposals below that demonstrate how this can be achieved.

<MobileNavigation
  trackEvent={tracker.trackEvent} // We only pass this one well known prop
  navItems={navItems}
/>

<DesktopNavigation
  trackEvent={tracker.trackEvent} We only pass this one well known prop
  navItems={navItems}
/>
// DestopHeader/NavItem.jsx
class NavItem extends React.Component {
  onMouseOver(e) => {
      const {onMouseOver, trackEvent} = this.props;
      // Only send a tracking event if a prop is passed
      // i.e a client can use this component without any analytics if they wish to
      trackEvent && trackEvent('onHover', {title: e.target.title});
      onMouseOver && onMouseOver(e);
  }
  ...other methods..
}

// MobileHeader/NavItem.jsx

class NavItem extends React.Component {
  onExpand(e) => {
      const {onMouseOver, trackEvent, title} = this.props;
      // Only send a tracking event if a prop is passed
      // i.e a client can use this component without any analytics if they wish to
      trackEvent && trackEvent('mobile-menu-expand', { title });
      onMouseOver && onMouseOver(e);
  }
  renderItem() {
    return (
      <li><button onClick={this.onExpand}></li>
    );
  }
}

The heavylifting of a client is fully isolated in 1 object - The tracker. Ive spent considerable time thinking about how an application like trailhead can create a tracker that can do

    1. Emit events to an anlytics endpoint.
    1. Handle hierarchical contexts.

There are 2 proposed interfaces for a tracker and its usage.

The 2 proposals demo the usage of a tracker where a differnet context is necessary for tracking clicks on cards in the recently viewed modules section vs a recommended module section

Proposal 1

// Card.jsx
onClickTitle() {
  const {trackEvent, title} = this.props;
  trackEvent && trackEvent('click', { title });
}

// App.jsx
import createTracker from 'lib/createTracker';
const config = {GA_KEY: 007};
const pageTracker = createTracker(config, {page: 'home'});

// RecentlyViewed.jsx
const { tracker } = this.props;
const recentlyViewedTracker = tracker.addContext({section: 'recently-viewed-cards'});

return [<Card trackEvent={tracker.trackEvent} ...other stuff />];
// When the event is fired, it will have the structure
/*
{
  eventName: 'click',
  title: 'Welcome to Swift',
  _parent: {
    section: 'recently-viewed-cards',
    _parent: {
      page: 'home'
    }
  }
}
*/



// RecommendedCards.jsx
const { tracker } = this.props;
const recommendedTracker = tracker.addContext({section: 'recommended-cards'});

return [<Card trackEvent={tracker.trackEvent} ...other stuff />];
// When the event is fired, it will have the structure
/*
{
  eventName: 'click',
  title: 'Welcome to Swift',
  _parent: {
    section: 'recommended-cards',
    _parent: {
      page: 'home'
    }
  }
}
*/

In short here's the interface for the tracker. Full implementation of the tracker interface is in this gist

const config = {GA_KEY: 007};

// Root context
const pageTracker = createTracker(config, {page: 'home'});

// When you want to create a child context
const recentlyViewedTracker = pageTracker.addContext({section: 'recently-viewed-cards'});
const { trackEvent } = recentlyViewedTracker; // pass this around

// In any child component
trackEvent('click', {title: 'Welcome to Swift'});

Proposal 2

This proposal differs from prposal 1 only in terms on the interface for creating the tracker. Everythign else is the same. so I am only going to show the relevant parts. Full implementation of this tracker interface is in this gist

const config = {GA_KEY: 007};

// Root context
const pageTracker = createTracker(config).setContext({page: 'home'});

// When you want to create a child context
const recentlyViewedTracker = createTracker(pageTracker).setContext({section: 'recently-viewed-cards'} );
const { trackEvent } = recentlyViewedTracker; // pass this around

// In any child component
trackEvent('click', {title: 'Welcome to Swift'});

From my perspective, Approach 2 is a lot more scalable and low mainteance for a client, solves the problem of context as well and keeps the interface for interacting with TDS very clean - by only introducing 1 additional prop to any component. Morever, a client can use tracker from the createTracker examples to track things outside of TDS, unifying all our analytics code across the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment