MCPcopy
hub / github.com/nytimes/react-tracking

github.com/nytimes/react-tracking @v9.3.2 sqlite

repository ↗ · DeepWiki ↗ · release v9.3.2 ↗
114 symbols 214 edges 23 files 0 documented · 0%
README

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>


 {
          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 (


 {
        trackEvent({ action: 'childClick' });
      }}
    />
  );
};

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

  return (
    <Track>
      <Child />


 {
          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 (


 {
        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:

```js 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

Core symbols most depended-on inside this repo

useTracking
called by 52
src/useTracking.js
trackEvent
called by 14
src/trackEventMethodDecorator.js
withTrackingComponentDecorator
called by 13
src/withTrackingComponentDecorator.js
trackEventMethodDecorator
called by 10
src/trackEventMethodDecorator.js
dispatchTrackingEvent
called by 5
src/dispatchTrackingEvent.js
makeClassMemberDecorator
called by 5
src/makeClassMemberDecorator.js
useTrackingImpl
called by 2
src/useTrackingImpl.js
WithTracking
called by 0
src/withTrackingComponentDecorator.js

Shape

Class 46
Function 40
Method 28

Languages

TypeScript100%

Modules by API surface

src/__tests__/e2e.test.js58 symbols
src/__tests__/hooks.test.js23 symbols
src/__tests__/withTrackingComponentDecorator.test.js9 symbols
src/__tests__/trackingHoC.test.js5 symbols
src/__tests__/trackEventMethodDecorator.test.js4 symbols
src/__tests__/makeClassMemberDecorator.test.js3 symbols
src/withTrackingComponentDecorator.js2 symbols
src/trackEventMethodDecorator.js2 symbols
src/makeClassMemberDecorator.js2 symbols
src/__tests__/useTracking.test.js2 symbols
src/useTrackingImpl.js1 symbols
src/useTracking.js1 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

Dependencies from manifests, versioned

@babel/cli7.19.3 · 1×
@babel/core7.19.3 · 1×
@babel/eslint-parser7.19.1 · 1×
@babel/plugin-proposal-class-properties7.18.6 · 1×
@babel/plugin-proposal-decorators7.19.3 · 1×
@babel/plugin-proposal-object-rest-spread7.19.4 · 1×
@babel/plugin-transform-runtime7.19.1 · 1×
@babel/preset-env7.19.4 · 1×
@babel/preset-react7.18.6 · 1×
babel-plugin-transform-decorators-legacy1.3.5 · 1×
deepmerge4.2.2 · 1×
enzyme3.11.0 · 1×

For agents

$ claude mcp add react-tracking \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact