• Stars
    star
    1,876
  • Rank 24,705 (Top 0.5 %)
  • Language
    JavaScript
  • License
    Other
  • Created almost 8 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐ŸŽฏ Declarative tracking for React apps.

react-tracking npm version

  • React specific tracking library, usable as a higher-order component (as @decorator or directly), or as a React Hook
  • Compartmentalize tracking concerns to individual components, avoid leaking across the entire app
  • Expressive and declarative (in addition to imperative) API to add tracking to any React app
  • Analytics platform agnostic

Read more in the Times Open blog post.

If you just want a quick sandbox to play around with:

Edit react-tracking example

Installation

npm install --save react-tracking

Usage

import track, { useTracking } from 'react-tracking';

Both @track() and useTracking() expect two arguments, trackingData and options.

  • trackingData represents the data to be tracked (or a function returning that data)
  • options is an optional object that accepts the following properties (when decorating/wrapping a component, it also accepts a forwardRef property):
    • dispatch, which is a function to use instead of the default dispatch behavior. See the section on custom dispatch() below.
    • dispatchOnMount, when set to true, dispatches the tracking data when the component mounts to the DOM. When provided as a function will be called in a useEffect on the component's initial render with all of the tracking context data as the only argument.
    • process, which is a function that can be defined once on some top-level component, used for selectively dispatching tracking events based on each component's tracking data. See more details below.
    • forwardRef (decorator/HoC only), when set to true, adding a ref to the wrapped component will actually return the instance of the underlying component. Default is false.
    • mergeOptions optionally provide deepmerge options, check deepmerge options API for details.

tracking prop

The @track() decorator will expose a tracking prop on the component it wraps, that looks like:

{
  // tracking prop provided by @track()
  tracking: PropTypes.shape({
    // function to call to dispatch tracking events
    trackEvent: PropTypes.func,

    // function to call to grab contextual tracking data
    getTrackingData: PropTypes.func,
  });
}

The useTracking hook returns an object with this same shape, plus a <Track /> component that you use to wrap your returned markup to pass contextual data to child components.

Usage with React Hooks

We can access the trackEvent method via the useTracking hook from anywhere in the tree:

import { useTracking } from 'react-tracking';

const FooPage = () => {
  const { Track, trackEvent } = useTracking({ page: 'FooPage' });

  return (
    <Track>
      <div
        onClick={() => {
          trackEvent({ action: 'click' });
        }}
      />
    </Track>
  );
};

The useTracking hook returns an object with the same getTrackingData() and trackEvent() methods that are provided as props.tracking when wrapping with the @track() decorator/HoC (more info about the decorator can be found below). It also returns an additional property on that object: a <Track /> component that can be returned as the root of your component's sub-tree to pass any new contextual data to its children.

Note that in most cases you would wrap the markup returned by your component with <Track />. This will deepmerge a new tracking context and make it available to all child components. The only time you wouldn't wrap your returned markup with <Track /> is if you're on some leaf component and don't have any more child components that need tracking info.

import { useTracking } from 'react-tracking';

const Child = () => {
  const { trackEvent } = useTracking();

  return (
    <div
      onClick={() => {
        trackEvent({ action: 'childClick' });
      }}
    />
  );
};

const FooPage = () => {
  const { Track, trackEvent } = useTracking({ page: 'FooPage' });

  return (
    <Track>
      <Child />
      <div
        onClick={() => {
          trackEvent({ action: 'click' });
        }}
      />
    </Track>
  );
};

In the example above, the click event in the FooPage component will dispatch the following data:

{
  page: 'FooPage',
  action: 'click',
}

Because we wrapped the sub-tree returned by FooPage in <Track />, the click event in the Child component will dispatch:

{
  page: 'FooPage',
  action: 'childClick',
}

Usage as a Decorator

The default track() export is best used as a @decorator() using the babel decorators plugin.

The decorator can be used on React Classes and on methods within those classes. If you use it on methods within these classes, make sure to decorate the class as well.

Note: In order to decorate class property methods within a class, as shown in the example below, you will need to enable loose mode in the babel class properties plugin.

import React from 'react';
import track from 'react-tracking';

@track({ page: 'FooPage' })
export default class FooPage extends React.Component {
  @track({ action: 'click' })
  handleClick = () => {
    // ... other stuff
  };

  render() {
    return <button onClick={this.handleClick}>Click Me!</button>;
  }
}

Usage on Stateless Functional Components

You can also track events by importing track() and wrapping your stateless functional component, which will provide props.tracking.trackEvent() that you can call in your component like so:

import track from 'react-tracking';

const FooPage = props => {
  return (
    <div
      onClick={() => {
        props.tracking.trackEvent({ action: 'click' });

        // ... other stuff
      }}
    />
  );
};

export default track({
  page: 'FooPage',
})(FooPage);

This is also how you would use this module without @decorator syntax, although this is obviously awkward and the decorator syntax is recommended.

Custom options.dispatch() for tracking data

By default, data tracking objects are pushed to window.dataLayer[] (see src/dispatchTrackingEvent.js). This is a good default if you use Google Tag Manager. However, please note that in React Native environments, the window object is undefined as it's specific to web browser environments. You can override this by passing in a dispatch function as a second parameter to the tracking decorator { dispatch: fn() } on some top-level component high up in your app (typically some root-level component that wraps your entire app).

For example, to push objects to window.myCustomDataLayer[] instead, you would decorate your top-level <App /> component like this:

import React, { Component } from 'react';
import track from 'react-tracking';

@track({}, { dispatch: data => window.myCustomDataLayer.push(data) })
export default class App extends Component {
  render() {
    return this.props.children;
  }
}

This can also be done in a functional component using the useTracking hook:

import React from 'react';
import { useTracking } from 'react-tracking';

export default function App({ children }) {
  const { Track } = useTracking(
    {},
    { dispatch: data => window.myCustomDataLayer.push(data) }
  );

  return <Track>{children}</Track>;
}

NOTE: It is recommended to do this on some top-level component so that you only need to pass in the dispatch function once. Every child component from then on will use this dispatch function.

When to use options.dispatchOnMount

You can pass in a second parameter to @track, options.dispatchOnMount. There are two valid types for this, as a boolean or as a function. The use of the two is explained in the next sections:

Using options.dispatchOnMount as a boolean

To dispatch tracking data when a component mounts, you can pass in { dispatchOnMount: true } as the second parameter to @track(). This is useful for dispatching tracking data on "Page" components, for example.

@track({ page: 'FooPage' }, { dispatchOnMount: true })
class FooPage extends Component { ... }
Example using hooks
function FooPage() {
  useTracking({ page: 'FooPage' }, { dispatchOnMount: true });
}

Will dispatch the following data (assuming no other tracking data in context from the rest of the app):

{
  page: 'FooPage'
}

Of course, you could have achieved this same behavior by just decorating the componentDidMount() lifecycle event yourself, but this convenience is here in case the component you're working with would otherwise be a stateless functional component or does not need to define this lifecycle method.

Note: this is only in effect when decorating a Class or stateless functional component. It is not necessary when decorating class methods since any invocations of those methods will immediately dispatch the tracking data, as expected.

Using options.dispatchOnMount as a function

If you pass in a function, the function will be called with all of the tracking data from the app's context when the component mounts. The return value of this function will be dispatched in componentDidMount(). The object returned from this function call will deepmerge with the context data and then dispatched.

A use case for this would be that you want to provide extra tracking data without adding it to the context.

@track({ page: 'FooPage' }, { dispatchOnMount: (contextData) => ({ event: 'pageDataReady' }) })
class FooPage extends Component { ... }
Example using hooks
function FooPage() {
  useTracking(
    { page: 'FooPage' },
    { dispatchOnMount: contextData => ({ event: 'pageDataReady' }) }
  );
}

Will dispatch the following data (assuming no other tracking data in context from the rest of the app):

{
  event: 'pageDataReady',
  page: 'FooPage'
}

Top level options.process

When there's a need to implicitly dispatch an event with some data for every component, you can define an options.process function. This function should be declared once, at some top-level component. It will get called with each component's tracking data as the only argument. The returned object from this function will deepmerge with all the tracking context data and dispatched in componentDidMount(). If a falsy value is returned (false, null, undefined, ...), nothing will be dispatched.

A common use case for this is to dispatch a pageview event for every component in the application that has a page property on its trackingData:

@track({}, { process: (ownTrackingData) => ownTrackingData.page ? { event: 'pageview' } : null })
class App extends Component {...}

...

@track({ page: 'Page1' })
class Page1 extends Component {...}

@track({})
class Page2 extends Component {...}
Example using hooks
function App() {
  const { Track } = useTracking(
    {},
    {
      process: ownTrackingData =>
        ownTrackingData.page ? { event: 'pageview' } : null,
    }
  );

  return (
    <Track>
      <Page1 />
      <Page2 />
    </Track>
  );
}

function Page1() {
  useTracking({ page: 'Page1' });
}

function Page2() {
  useTracking({});
}

When Page1 mounts, event with data {page: 'Page1', event: 'pageview'} will be dispatched. When Page2 mounts, nothing will be dispatched.

Note: The options.process function does not currently take single-page app (SPA) navigation into account. If the example above were implemented as an SPA, navigating back to Page1, with no page reload, would not cause options.process to fire a second time even if the Page1 component remounts. The recommended workaround for now is to call trackEvent manually in a React.useEffect callback in child components where you want the data to fire (see this code sandbox for an example). Follow issue #189 to monitor progress on a fix.

Tracking Asynchronous Methods

Asynchronous methods (methods that return promises) can also be tracked when the method has resolved or rejects a promise. This is handled transparently, so simply decorating an asynchronous method the same way as a normal method will make the tracking call after the promise is resolved or rejected.

// ...
  @track()
  async handleEvent() {
    return await asyncCall(); // returns a promise
  }
// ...

Or without async/await syntax:

// ...
  @track()
  handleEvent() {
    return asyncCall(); // returns a promise
  }

Advanced Usage

You can also pass a function as an argument instead of an object literal, which allows for some advanced usage scenarios such as when your tracking data is a function of some runtime values, like so:

import React from 'react';
import track from 'react-tracking';

// In this case, the "page" tracking data
// is a function of one of its props (isNew)
@track(props => {
  return { page: props.isNew ? 'new' : 'existing' };
})
export default class FooButton extends React.Component {
  // In this case the tracking data depends on
  // some unknown (until runtime) value
  @track((props, state, [event]) => ({
    action: 'click',
    label: event.currentTarget.title || event.currentTarget.textContent,
  }))
  handleClick = event => {
    if (this.props.onClick) {
      this.props.onClick(event);
    }
  };

  render() {
    return <button onClick={this.handleClick}>{this.props.children}</button>;
  }
}

NOTE: That the above code utilizes some of the newer ES6 syntax. This is what it would look like in ES5:

// ...
  @track(function(props, state, args) {
    const event = args[0];
    return {
      action: 'click',
      label: event.currentTarget.title || event.currentTarget.textContent
    };
  })
// ...

When tracking asynchronous methods, you can also receive the resolved or rejected data from the returned promise in the fourth argument of the function passed in for tracking:

// ...
  @track((props, state, methodArgs, [{ value }, err]) => {
    if (err) { // promise was rejected
      return {
        label: 'async action',
        status: 'error',
        value: err
      };
    }
    return {
      label: 'async action',
      status: 'success',
      value // value is "test"
    };
  })
  handleAsyncAction(data) {
    // ...
    return Promise.resolve({ value: 'test' });
  }
// ...

If the function returns a falsy value (e.g. false, null or undefined) then the tracking call will not be made.

Accessing data stored in the component's props and state

Further runtime data, such as the component's props and state, are available as follows:

  @track((props, state) => ({
    action: state.following ? "unfollow clicked" : "follow clicked",
    name: props.name
  }))
  handleFollow = () => {
     this.setState({ following: !this.state.following })
    }
  }

Example props.tracking.getTrackingData() usage

Any data that is passed to the decorator can be accessed in the decorated component via its props. The component that is decorated will be returned with a prop called tracking. The tracking prop is an object that has a getTrackingData() method on it. This method returns all of the contextual tracking data up until this point in the component hierarchy.

import React from 'react';
import track from 'react-tracking';

// Pass a function to the decorator
@track(() => {
  const randomId = Math.floor(Math.random() * 100);

  return {
    page_view_id: randomId,
  };
})
export default class AdComponent extends React.Component {
  render() {
    const { page_view_id } = this.props.tracking.getTrackingData();

    return <Ad pageViewId={page_view_id} />;
  }
}

Note that if you want to do something like the above example using the useTracking hook, you will likely want to memoize the randomId value, since otherwise you will get a different value each time the component renders:

import React, { useMemo } from 'react';
import { useTracking } from 'react-tracking';

export default function AdComponent() {
  const randomId = useMemo(() => Math.floor(Math.random() * 100), []);
  const { getTrackingData } = useTracking({ page_view_id: randomId });
  const { page_view_id } = getTrackingData();

  return <Ad pageViewId={page_view_id} />;
}

Tracking Data

Note that there are no restrictions on the objects that are passed in to the decorator or hook.

The format for the tracking data object is a contract between your app and the ultimate consumer of the tracking data.

This library simply merges (using deepmerge) the tracking data objects together (as it flows through your app's React component hierarchy) into a single object that's ultimately sent to the tracking agent (such as Google Tag Manager).

TypeScript Support

You can get the type definitions for React Tracking from DefinitelyTyped using @types/react-tracking. For an always up-to-date example of syntax, you should consult the react-tracking type tests.

PropType Support

The props.tracking PropType is exported for use, if desired:

import { TrackingPropType } from 'react-tracking';

Alternatively, if you want to just silence proptype errors when using eslint react/prop-types, you can add this to your eslintrc:

{
  "rules": {
    "react/prop-types": ["error", { "ignore": ["tracking"] }]
  }
}

Deepmerge

The merging strategy is the default deepmerge merging strategy. We do not yet support extending the deepmerge options. If you're interested/have a need for that, please consider contributing: #186

You can also use/reference the copy of deepmerge that react-tracking uses, as it's re-exported for convenience:

import { deepmerge } from 'react-tracking';

Old Browsers Support

Going forward from version 9.x, we do not bundle core-js (ES6 polyfills) anymore. To support old browsers, please add core-js to your project.

More Repositories

1

covid-19-data

A repository of data on coronavirus cases and deaths in the U.S.
6,989
star
2

objective-c-style-guide

The Objective-C Style Guide used by The New York Times
5,848
star
3

gizmo

A Microservice Toolkit from The New York Times
Go
3,753
star
4

Store

Android Library for Async Data Loading and Caching
Java
3,531
star
5

NYTPhotoViewer

A modern photo viewing experience for iOS.
Objective-C
2,847
star
6

pourover

A library for simple, fast filtering and sorting of large collections in the browser. There is a community-maintained fork that addresses a handful of post-NYT issues available via @hhsnopek's https://github.com/hhsnopek/pourover
JavaScript
2,393
star
7

kyt

Starting a new JS app? Build, test and run advanced apps with kyt ๐Ÿ”ฅ
JavaScript
1,922
star
8

ice

track changes with javascript
JavaScript
1,708
star
9

backbone.stickit

Backbone data binding, model binding plugin. The real logic-less templates.
JavaScript
1,641
star
10

library

A collaborative documentation site, powered by Google Docs.
JavaScript
1,143
star
11

openapi2proto

A tool for generating Protobuf v3 schemas and gRPC service definitions from OpenAPI specifications
Go
940
star
12

gziphandler

Go middleware to gzip HTTP responses
Go
857
star
13

svg-crowbar

Extracts an SVG node and accompanying styles from an HTML document and allows you to download it all as an SVG file.
JavaScript
840
star
14

ingredient-phrase-tagger

Extract structured data from ingredient phrases using conditional random fields
Python
784
star
15

Emphasis

Dynamic Deep-Linking and Highlighting
JavaScript
576
star
16

tamper

Ruby
499
star
17

three-loader-3dtiles

This is a Three.js loader module for handling OGC 3D Tiles, created by Cesium. It currently supports the two main formats, Batched 3D Model (b3dm) - based on glTF Point cloud.
TypeScript
444
star
18

react-prosemirror

A library for safely integrating ProseMirror and React.
TypeScript
418
star
19

rd-blender-docker

A collection of Docker containers for running Blender headless or distributed โœจ
Python
415
star
20

Register

Android Library and App for testing Play Store billing
Kotlin
381
star
21

text-balancer

Eliminate typographic widows and other type crimes with this javascript module
JavaScript
373
star
22

document-viewer

The NYTimes Document Viewer
JavaScript
310
star
23

ios-360-videos

NYT360Video plays 360-degree video streamed from an AVPlayer on iOS.
Objective-C
273
star
24

three-story-controls

A three.js camera toolkit for creating interactive 3d stories
TypeScript
247
star
25

backbone.trackit

Manage unsaved changes in a Backbone Model.
JavaScript
202
star
26

aframe-loader-3dtiles-component

A-Frame component using 3D-Tiles
JavaScript
187
star
27

marvin

A go-kit HTTP server for the App Engine Standard Environment
Go
177
star
28

drone-gke

Drone plugin for deploying containers to Google Kubernetes Engine (GKE)
Go
165
star
29

Chronicler

A better way to write your release notes.
JavaScript
162
star
30

nginx-vod-module-docker

Docker image for nginx with Kaltura's VoD module used by The New York Times
Dockerfile
161
star
31

collectd-rabbitmq

A collected plugin, written in python, to collect statistics from RabbitMQ.
Python
143
star
32

public_api_specs

The API Specs (in OpenAPI/Swagger) for the APIs available from developer.nytimes.com
136
star
33

gunsales

Statistical analysis of monthly background checks of gun purchases
R
130
star
34

gcp-vault

A client for securely retrieving secrets from Vault in Google Cloud infrastructure
Go
119
star
35

rd-bundler-3d-plugins

Bundler plugins for optimizing glTF 3D models
JavaScript
119
star
36

Fech

Deprecated. Please see https://github.com/dwillis/Fech for a maintained fork.
Ruby
115
star
37

data-training

Files from the NYT data training program, available for public use.
114
star
38

drone-gae

Drone plugin for managing deployments and services on Google App Engine (GAE)
Go
97
star
39

mock-ec2-metadata

Go
95
star
40

encoding-wrapper

Collection of Go wrappers for Video encoding cloud providers (moved to @video-dev)
Go
85
star
41

redux-taxi

๐Ÿš• Component-driven asynchronous SSR in isomorphic Redux apps
JavaScript
70
star
42

video-captions-api

Agnostic API to generate captions for media assets across different transcription services.
Go
61
star
43

lifeline

A cron-based alternative to running daemons
Ruby
58
star
44

gcs-helper

Tool for proxying and mapping HTTP requests to Google Cloud Storage (GCS).
Go
54
star
45

logrotate

Go
54
star
46

httptest

A simple concurrent HTTP testing tool
Go
48
star
47

kyt-starter-universal

Deprecated, see: https://github.com/NYTimes/kyt/tree/master/packages/kyt-starter-universal
JavaScript
33
star
48

nytcampfin

A thin Python client for The New York Times Campaign Finance API
Python
27
star
49

safejson

safeJSON provides replacements for the 'load' and 'loads' methods in the standard Python 'json' module.
Python
27
star
50

thumbor-docker-image

Docker image for Thumbor smart imaging service
26
star
51

times_wire

A thin Ruby client for The New York Times Newswire API
Ruby
26
star
52

haiti-debt

Historical data on Haitiโ€™s debt payments to France collected by The New York Times.
21
star
53

hhs-child-migrant-data

Data from the U.S. Department of Human Health and Services on children who have migrated to the United States without an adult.
21
star
54

jsonlogic

Clojure
20
star
55

elemental-live-client

JS library to communicate with Elemental live API.
JavaScript
19
star
56

Open-Source-Science-Fair

The New York Times Open Source Science Fair
JavaScript
19
star
57

tweetftp

Ruby Implementation of the Tweet File Transfer Protocol (APRIL FOOLS JOKE)
Ruby
19
star
58

prosemirror-change-tracking-prototype

JavaScript
18
star
59

plumbook

Data from the Plum Book, published by the GPO every 4 years
17
star
60

libvmod-queryfilter

Simple querystring filter/sort module for Varnish Cache v3-v6
M4
16
star
61

sneeze

Python
16
star
62

querqy-clj

Search Query Rewriting for Elasticsearch and more! Built on Querqy.
Clojure
14
star
63

sqliface

handy interfaces and test implementations for Go's database/sql package
Go
14
star
64

grocery

The grocery package provides easy mechanisms for storing, loading, and updating Go structs in Redis.
Go
13
star
65

oak-byo-react-prosemirror-redux

JavaScript
13
star
66

vase.elasticsearch

Vase Bindings for Elasticsearch
Clojure
11
star
67

library-customization-example

An example repo that customizes Library behavior
SCSS
11
star
68

counter

count things, either as a one-off or aggregated over time
Ruby
11
star
69

kyt-starter

The default starter-kyt for kyt apps.
JavaScript
10
star
70

tulsa-1921-data

Data files associated with our story on the 1921 race massacre in Tulsa, Oklahoma.
10
star
71

open-blog-projects

A repository for code examples that are paired with our Open Blog posts
Swift
9
star
72

rd-mobile-pg-demos

HTML
9
star
73

pocket_change

Python
9
star
74

mentorship

7
star
75

sort_by_str

SQL-like sorts on your Enumerables
Ruby
7
star
76

drone-gdm

Drone.io plugin to facilitate the use of Google Deployment Manager in drone deploy phase.
Go
6
star
77

kyt-starter-static

Deprecated, see: https://github.com/NYTimes/kyt/tree/master/packages/kyt-starter-static
JavaScript
6
star
78

s3yum

Python
5
star
79

pocket

Python
5
star
80

drone-openapi

A Drone plugin for publishing Open API service specifications
Go
5
star
81

threeplay

Go client for the 3Play API.
Go
4
star
82

amara

Amara client for Go
Go
4
star
83

go-compare-expressions

Go
3
star
84

kaichu

Python
3
star
85

license

NYT Apache 2.0 license
3
star
86

prosemirror-tooltip

JavaScript
2
star
87

photon-dev_demo

A "Sustainable Systems, Powered By Python" Demo Repository (1 of 3)
Shell
2
star
88

std-cat

Content Aggregation Technology โ€” a standard for content aggregation on the Web
HTML
1
star