Hello Treacker, a tracking library for React
Tags: react, tracking, javascript
Published: 2019-11-10 20:10:46

## TL;DR:
I built a small (1.5kb) and performant event tracking library, that lets you connect with any tracking provider (GA, MixPanel, etc) with its simple API.

- Demo: https://codesandbox.io/embed/sharp-rain-jr0m6?fontsize=14
- Library: https://www.npmjs.com/package/treacker
- Docs: https://github.com/Badiapp/treacker

## Why another tracking library

Tracking solutions like [React tracker](https://github.com/faouzioudouh/react-tracker#readme) or [React tracking](https://github.com/nytimes/react-tracking) solve the tracking the challenge coming for a perspective that data is present at the moment the tracking event is being triggered, meaning that the data needed to track an event is available from time 0.

Which in reality it's not. Let's see the following example:

``` jsx
const Component = ({ userId, roomId }) => {
  const tracking = useTracking()
  useEffect(() => {
    tracking.trackEvent({ action: 'invite_sent', userId, roomId })
  }, [])
  
  // the rest of my component
}
```

But, what if because of the architecture of the application, the asynchronous nature of nowadays applications (or any other reason)the `userId` or `roomId` values are not available when mounting the component, the tracking event won't report the correct data.

Having a condition could fix the problem:

``` jsx
const Component = ({ userId, roomId }) => {
  const tracking = useTracking()
  useEffect(() => {
    if(!userId || !roomId) return
    tracking.trackEvent({ action: 'invite_sent', userId, roomId })
  }, [userId, roomId])
  
  // the rest of my component
}
```

But I will need to do this do it over and over across the application, this starts to be unmaintainable and too verbose. Instead, what if there could be a way to let the "tracking system" manage that for us, what if the data integrity is part of the responsibilities of this "tracking system".

## The proposal

I want to create a tool that:
- Works with vanilla JS and React is just an abstraction, so it's not dependant of React architecture constraints.
- Its responsibility is to ensure the tracking data integrity
- Provides a declarative interface
- It is agnostic of the transport service is used on the project to track the events
- Has a simple, yet powerful interface

## Say hello to Treacker

(Tracking + React) = Treacker 🤯

Treacker takes the following assumptions on the application architecture:
- The part of the code in which the "tracking provider" is declared knows about the base data that will need to be tracked (ex. userId, userRole, appVersion), let's say, the global/high-level data


Said that let's see how it works:
1. Declare a `TrackingProvider`
2. Everything you want to track should be inside the Provider tree
3. Access the provider either using `TrackingContext` or `useTracking` hook

## Demo
{% codesandbox sharp-rain-jr0m6 %}

## Example
- I have will request `getRooms` and received as a list `rooms` as props, in which I will track the mounting of each room on the list
- I have a component that will show info on a user called `UserComponent` that will be shown after doing a request to the server in which I will track when mounting

``` js
import { useState, useEffect } from 'react'
import { TrackingProvider } from 'treacker'

import UserComponent from './user-component'
import Room from './room'

const INITIAL_PARAMS = {
  locale: 'en',
  app_version: 1
}

const handleOnTrackingEvent = event => {
  // do stuff when the event has been fired.
  // like reporting to Google Analytics or Mixpanel
  // signature { eventName, params, timestamp }
}

const Layout = ({ getUser, getRoom, rooms }) => {

  const [ready, setReady] = useState(false)
  const [params, setParams] = useState(INITIAL_PARAMS)
  useEffect(() => {
    getUser().then((user) => {
      // update the parameters for the provider
      setParams(state => ({
        ...state,
        userRole: user.role,
        userId: user.id,
      })
      setReady(true)
    })

    getRoom()
  }, [])
  return (
    <TrackingProvider params={params} onTrackingEvent={handleOnTrackingEvent} isReady={ready}>
      <UserComponent {...user} />
      {
        rooms.map(room => <Room {...room} />)
      }
    </TrackingProvider>
  )
}
```

The `UserComponent`:
``` js
import { useEffect } from 'react'
import { useTracking } from 'treacker'

const UserComponent = () => {
  const tracking = useTracking()
  useEffect(() => {
    tracking.track('user-component.loaded')
  }, [])

  return (
    // ... the component implementation
  )
}
```


Then the room component:

``` js
import { useEffect } from 'react'
import { useTracking } from 'treacker'

const Room = ({ roomId }) => {
  const tracking = useTracking()
  useEffect(() => {
    tracking.track('room.loaded', { roomId })
  }, [])

  return (
    // ... the component implementation
  )
}
```

## So what's happening here?
- `TrackingProvider` has 3 main props:
1. `onTrackingEvent`, which will be invoked each time there is a tracking event
2. `params`, this is going to be the global parameters that will be sent with each event
3. `isReady` is the flag that will let know when it's "safe" to dispatch the events

For more info on how the event signature looks like, [check the docs](https://github.com/Badiapp/treacker#track).

Even if the data is not ready, for example like `UserComponent` that mounts before fetching the userData, the events stay in a queue and are dispatched only after knowing that is safe by the `isReady` flag on `TrackingProvider`.

## More about the interface

### withTracking
The library exposes also a [HOC `withTracking`](https://github.com/Badiapp/treacker#hoc) which is useful when the component is not part of the `TrackingProvider` tree.

### registering listeners
In case you need to register more event listeners to the trackingProvider, it's possible using [`registerListener`](v#registering-event-callbacks).

## Final words
I found the approach to be useful in my use case and that's why I thought to share it, hopefully it will be useful for you too!


[Check the docs](https://github.com/Badiapp/treacker#docs) for more details, or the demo in [codesandbox](https://codesandbox.io/s/sharp-rain-jr0m6?fontsize=14).