Tracking in React Apps

Disclaimer

  • The code might not be a best practice, because it's based on personal experience.
  • Example has been simplified, so we could focus on the tracking code and tools
  • This post will not discuss or used any 3rd party implementation of specific tracking platform (crashlytics, data dog, sentry, mixpanel, etc)
  • The 3rdParty Mocked code might be different from real 3rdParty API

Notice

if you are interested in the application code more than the tracking implementation. Leave reaction to this post, I'll consider making another post to explain it.

Tracking

Nowadays, tracking user experience is a must for most application, by collecting the tracked data from user we can develop, fixing or improve our feature (especially UX).

Based on my experience tracking can be differ into 2 category :

  • product / marketing: this tracking goals is to keep track and evaluate marketing approaches (FB ads, google ads, instagram link, etc), and help product team to evaluate UX
  • error: this tracking purpose is to notify developer about the error that occur in production before customer making any complain.

Let's see the implementation in react code

Application and tracking statement

To implements tracking we need to at least having an application. I have create a base project at:

What is the app functionality ?

  • a news curation app that use newsapi
  • there is 2 tab Home and Top News
  • Each tab have refresh news functionality
  • Each news card linked to respective article website

What are we going to track ?

  • track every click on go to source button, we want to evaluate whether user usually go to tops news tab or not, so the Data expected looks like :
{
  eventName: 'click_go_to_source',
  page: 'Home / TopNews'
}
  • track every click on refresh feed button, we want to evaluate whether user click refresh feed button or not so the data expected looks like :
{
  eventName: 'refresh_feed',
  page: 'Home / TopNews'
}
  • track error when fetching data, we want to track every error occur when fetching data. Data expect to looks like :
{
  eventName: 'error_fetch',
  page: 'Home / TopNews',
  errorReason: stringify error object
}

Usual Tracking (Direct Approach)

Basically it's just calling 3rd party sdk / api for event tracking or logging on every click handler or error catch

In this code example we will use Mocked DataDog for our error tracking and MixPanel for our click tracking.

The code implementation can be seen in link.

Detail Code go through

Click Go To Source Track
every time the user click go to source this code will send over the data to mock MixPanel.

// ArticleCard.js
...
// line 7
const handleClick = () => {
  const eventName = "click_go_to_source";
  const unique_id = uuid();
  MixPanel.track(eventName, unique_id, {
    page,
  });
  ...
};
....

Click Refresh Feed Track
every time the user click refresh feed this code will send over the data to mock MixPanel.

// Home.js or TopNews.js
...
// line 26
const onRefreshClick = () => {
  const eventName = "refresh_feed";
  const unique_id = uuid();
  MixPanel.track(eventName, unique_id, {
    page,
  });
  ...
};
....

Fetch News error Track
every time our fetch to news from newsapi failed, this code will send over the fetch_error data to mock DDlog.

// Home.js or TopNews.js
...
// line 15
onError: (err) => {
  const eventName = "error_fetch";
  DDlog.error(eventName, {
    page,
    errorReason: JSON.stringify(err, null, 4),
  });
},
....

It seems everything to work fine ๐Ÿค”, yep that's what i thought, until some changes was needed because of new feature or 3rd Party tracking platform commercial issue / fees.

Imagine that we already put 100+ tracker over 10 screens, then we need to :

  • change tracking platform, for example from MixPanel to Heap. we need to manually refactor all of our MixPanel tracking code 1-by-1 ๐Ÿ˜ตโ€๐Ÿ’ซ.
  • add additional tracking data since we have new login feature, now we want to track user data every too ๐Ÿคฏ.

Gratefully, i encounter this problem when my tracker was still less than 20 ๐Ÿ˜ฎโ€๐Ÿ’จ. But there is a question pop up on my mind, do i need to change the code one-by-one every time there is commercial issue or new feature that affect current tracking ?

React Tracking

That's what lead me to react-tracking by NYT, a React specific tracking library. it helps to :

  • Centralize our tracking logic, yet compartmentalize tracking concerns to individual components
  • Give tracking data a scope

Let's see the code implementation link.

We create ReactTrackingInitializer HOC (High Order Component) to be our parent / root tracking wrapper.

const ReactTrackingInitializer = ({ children }) => {
  const { Track } = useTracking(
    {
      // this is where the initialize data put
      trackVersion: "1.0.0",
    },
    {
      dispatch: (trackedData) => {
        console.log("dispatchData", trackedData);  
    }
  );
  return <Track>{children}</Track>;
};

useTracking is a hooks version to implementing react-tracking which suitable for functional component, find out more on their docs if you still implementing class component.

useTracking takes 2 params:

  1. initial data, means this data available for the rest of the child component.
  2. is the options which consist of dispatch,dispatchOnMount,process, and fowardRef more detail check react-tracking

useTracking will return object with 3 properties:

  1. trackEvent: a function to send data to be process at process, then dispatch.
  2. getTrackingData: a function that return current initial data in our tracker.
  3. Track: a HOC that wrapped a child component to give scope to it's initial data, process and dispatch logic. which later can be triggered using trackEvent

From the reference we can implements our 3rd Party logic at dispatch option. so it will looks like this :

...
dispatch: (trackedData) => {
  console.log("dispatchData", trackedData);
  const { eventName, ...restOfData } = trackedData.data;
  switch (trackedData.type) {
     case "product":
       const unique_id = uuid();
       MixPanel.track(eventName, unique_id, restOfData);
       break;
     case "error":
       DDlog.error(eventName, restOfData);
       break;
     default:
       break;
  }
},
...

It looks a lot like redux reducers. Now you might ask there must be a dispatch mechanism to like redux, where is it ? checkout the code at Home.js line 25 - 33

const { trackEvent, Track } = useTracking({
  data: { page: "HOME" },
});

const onRefreshClick = () => {
  trackEvent({ type: "product", data: { eventName: "refresh_feed" } });
  refetch();
};

the trackEvent will send over the data below to our dispatch function.

{ 
  type: "product", 
  data: { 
    eventName: "refresh_feed",
    page: "HOME"
  } 
  trackVersion: "1.0.0"
}

Wait, Where did trackVersion: "1.0.0" and page: "HOME" came from ๐Ÿ™„ ? react tracking perform a merge operation on data we sent and initial data provided. in this case :

  • data we send :
{ 
  type: "product", 
  data: { 
    eventName: "refresh_feed"
  } 
}
  • initial value on Home.js useTracking :
{ 
  data: { 
    page: "HOME"
  } 
}
  • initial value on ReactTrackingInitializer useTracking:
{
  trackVersion: "1.0.0"
}

We already utilize react-tracking ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰, Just Note that:

  • there must be at least 1 component that wrapping with <Track></Track> at root level (prefer to wrap )
  • Initial value only available to child component if we wrapped them with <Track></Track>. that why we wrapped <ArticleCard> in Home.js line 57 - 63, so it get the initial value from Home.js useTracking, otherwise it will only have initial value of ReactTrackingInitializer.js.

Now back to the problem, let say we need to:

  1. change MixPanel to Heap
  2. add user data to every tracker, because we have new login feature

just see the difference between branch rtracking and rtracking-solution.

Changes need #1

Changes need to solve the problem statement:

  1. change MixPanel to Heap
  2. add user data, because we have add login feature

and compare it to the difference between branch direct and direct-solution`.

Changes Need -> Direct Solution #2

Changes need to solve the problem statement:

change MixPanel to Heap add user data, because we have add login feature

It will more work to be done when using 3rdParty Sdk / API directly, Imagine we have 10+ MixPanel tracker, it will cost a lot of time.

Conclusion

React Tracking Help us to centralize the tracking logic so if there are any changes needed we can just refactor our dispatch function.

Thanks for reading, leave any comment below ๐Ÿ˜Š

Shout Out

GitHub logo nytimes / react-tracking

๐ŸŽฏ Declarative tracking for React apps.

16