Git Product home page Git Product logo

react-tracking's Introduction

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.

react-tracking's People

Contributors

adi518 avatar aneudy1702 avatar anthonycrowcroft avatar benlorantfy avatar bgergen avatar brkalow avatar chimurai avatar damassi avatar dependabot[bot] avatar dortzur avatar frytyler avatar gedeagas avatar geekyshiva avatar glebpigulevsky avatar ivankravchenko avatar kimskovhusandersen avatar lszm avatar maxrbaldwin avatar mckernanin avatar mennenia avatar nathanchapman avatar orta avatar paradeto avatar rickh18 avatar schustafa avatar tanhauhau avatar tizmagik avatar williardx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-tracking's Issues

HOC Process does not apply to methods

I've added a HOC @track which has a process and it works great for the @track on the class, but does not apply to the methods in the class.

I'm using this to validate events before they're sent to the dataLayer and change the case on some certain attributes so it's ideal if this is applied to all events.

Multiple options.process behavior

Currently we encourage to use only one process on top level component, by making checks in constructor method of every tracking HoC

      constructor(props, context) {
        super(props, context);

        if (context.tracking && context.tracking.process && process) {
          console.error('[nyt-react-tracking] options.process should be used once on top level component');
        }
      }

Could these checks be performance consuming?

If there are several usages of options.process down a component tree desired behavior should be:

  • display error message (current behavior)
  • child's process should override parent's
  • merge return values of each
  • do nothing, because everyone should read documentation ;)
  • ...?

[React Native][Android] transform inlineRequires cause maximum call stack

Hello, I using react-tracking in my react native projects.

But when I enable the transform: { inilineRequires: true } the project breaks. I found out if I comment the react-tracking, it works as expected. Note that it only affected in android only.

Example Repo

https://github.com/Jekiwijaya/react-tracking-bug-inline-require-react-native

The bug is make maximum call stack / recursive in some line.

I think the problem comes from core-js, which override the defineProperty, but I'm not really sure.

Return rejected promises

After updating from 5.3.0 > 5.6.0 the decorator now does not return the value of a promise.

@track(props => ({
  action: 'FormSubmit',
  label: props.name || null,
}))
handleSubmit = values => this.props.onSubmit(values).then(() => {}, err => err);

The onSubmit function above returns a promise which returns an error to display submission errors via react-final-form. If the decorator is used this error does not get reported as the value isn't passed through.

Submit a failing test case

Submit a failing test case for a normal usage of the react-tracking API. Note, a failing test case is enough, you don't necessarily need to solve it.

Hint: It's likely possible to do this for #36

CONTRIBUTING.md

Just noticed there is no CONTRIBUTING.md file to help people get started with the project.

Kent C Dodds talks about a great way to incorporate contributing information here: https://github.com/kentcdodds/all-contributors

Even just a simple contributing.md file talking about setup and expectations of contributing would be helpful for anyone getting involved.

Docs website

It would be nice to use gh-pages to put the documentation that's currently completely in the README in a nicer place

Handling before/after events

First of all, this solution looks excellent โœจ At Artsy we were in the process of designing a similar solution, but happy to stop that effort in favour of using yours (both in React and React Native).

One thing that we did include in our design and yours doesnโ€™t seem to have, but please correct me if I overlooked it, is the possibility to send before/after events when the decorated function performs e.g. an API call. Our design for this was that if the decorated function returns a Promise weโ€™d tap into that and fire the tracking function both before and after (possibly with error) the promise finishes.

Would you accept a PR for that? If so, weโ€™ll start using your lib and start porting our code over to yours.

Create FAQ

We get a lot of similar questions, usually around the nuances of dispatch, process and how contextual tracking propagates through the app. It would be helpful to create an FAQ and/or "common patterns" doc to explain these (with examples) in a little more detail.

We could use the question label as a good start for this documentation.

Formatter

Is it possible to run all events through a formatter function before they're sent to the dataLayer?

A use-case for this is that I would like to ensure that certain field values are all formatted the same. This means that in some instances "search" & "Search" is split into two different events.

If a formatter was applied to those, you could enforce a lowercase standard to all events where certain fields are formatted differently to ensure consistency.

Proposed implementation:

@track({ category: 'App' }, {
  formatter: trackingData => ({
    ...trackingData,
    label: (trackingData.label || '').toLowerCase(),
    value: trackingData.value || null,
  }),
})

I'd assume it would work like process where there's only 1 formatter allowed but I suppose someone could have another use-case which would require them to format their events on a component by component basis.

window.dataLayer is undefined

It seems that the dataLayer is not being initialized in src/dispatchTrackingEvent.js. After I trigger events through any @track decorator, when I log window.dataLayer, it show up in the console as undefined. See below:

import React, { Component } from 'react';
import track from "react-tracking";
import '../../App.scss';
import './locator.scss';
import LocatorContent from '../../Components/LocatorContent'
import Search from '../../Components/Search/Search'

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

  @track({ action: 'click' })
  handleClick = () => {
    // setTimeout ensures the console.log happens after dataLayer has been updated
    // so that we can inspect the data that was pushed
    setTimeout(() => { 
      console.log(window);
      console.log('click track', window.dataLayer);
    }, 0);
  };
  render() {
    return (
      <div className="Locator"
        onClick={this.handleClick}
      >
        <Search/>
        <LocatorContent/>
      </div>
    );
  }
}

export default Locator;

React Hooks API?

Given that React Hooks are now here to stay and more and more people are eager to use them, I was wondering if you have started thinking about a future version of react-tracking that would integrate well with this new landscape?

Specifically, we canโ€™t add decorators to functions and Iโ€™d really like to keep separating business logic from inline analytics callsโ€“while keeping them clearly โ€˜attachedโ€™ to it.

Duplicated data entries and parent components challenges

Dear NYTimes team, dear @tizmagik,

we are currently working on a relaunch of our online-shop/platform with ReactJs. For tracking purposes we want to use your powerful, light and well implemented plugin. Before we go online, we have a few challenges to overcome. Therefore, we would be very happy if you could share your experiences with us:

Specifically, since ReactJs's lifecycle calls methods more than only once to varify if there are dom-changes, the tracking decorator is triggered more than once too. This results in duplicated entries on the dataLayer. Do you have any solution for this? Or do get the duplicate data filtered by you data analyst? Do get the data filtered by comparing milliseconds in timestamps?

The second challenge for which we do not yet have a solution: Tracking parent components. For a better explanation here is a short example: We display multiple products on one page grouped by sliders named "recommendations" and "other-users-liked". It may happen that one product appears in both sliders at the same time. A click on the product then triggers a tracking action. Because we have component based architecture, the product component doesn't know anything from the parent slider. But we do need to get this information in which slider the product has been to ensure proper analytics on which slider is more successful. Did you have similar problems? If so, how did you solve this?

We would be very happy and grateful if you could share your experiences with us.

Best wishes,
Daniel

dispatchOnMount: true has no effect when `process()` ends up not dispatching

Description

When there is a top-level process function defined and dispatchOnMount: true is passed in and the result of calling process() is to not dispatch, then the boolean for dispatchOnMount essentially has no effect.

Expected Behavior

When dispatchOnMount: true is passed in, it should always dispatch when the component mounts, unless the call to process() has dispatched, then it shouldn't (because we wouldn't want to dispatch the same data twice).

Actual Behavior

When dispatchOnMount: true is passed in, and there is a process() function defined (that happens to not dispatch), then that flag is essentially ignored.

The root cause of the issue is the conditional logic here:

https://github.com/NYTimes/react-tracking/blob/585da2754dd63475dfb61d69dcebf3da18ebbef3/src/withTrackingComponentDecorator.js#L68-L88

h/t @callihiggins

Documentation: FooComponent is missing in the sample

In the "Usage on Stateless Functional Components" section of readme, you have created a component with the name "FooPage", but while creating HOC thorugh "track" you have mentioned "FooComponent".

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

It should be the following;

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

An updated release for React 16

I really appreciate you putting this library out there!

I'm a little stuck at the moment though because, while the latest published version does indeed support React 16, the peer dependency of react@^15.0.0 doesn't meet semver package requirements when using it alongside React 16, and a yarn check yields the following with [email protected]:

...
error "package#react-tracking#react@^15.0.0" doesn't satisfy found match of "[email protected]"
...

It looks to be all fixed up with with #72, so I was hoping a new release could be published that matches what's in master. Thanks!

Add test for `@track((props, [event]) => {})` signature

#45 revealed that we're missing a test for this use case/function signature mentioned in the docs

  // In this case the tracking data depends on
  // some unknown (until runtime) value
  @track((props, [event]) => ({ // NOTE: This PR would change this function signature
    action: 'click',
    label: event.currentTarget.title || event.currentTarget.textContent
  }))
  handleClick = (event) => {
    if (this.props.onClick) {
      this.props.onClick(event);
    }
  }

Unable to run sandbox code

Hello ,
I tried same to execute code by downloading code from sandbox download link .
While running exception gives Unexpected token @track.

Track Hierarchy

Hi,

Given: a tracked component tree like:

App @track({app:'myApp'})
|--Page @track({page:'page1'})
    |-TabBar @track({tabbar:'tabbar1'})
       |-Tab @track({tab:'tab1'})
       |-Tab @track({tab:'tab2'})

When: the user selects a Tab1

Then: I would like to be able to dispatch a message like:
{event: 'myapp.page1.tabbar1.tab1'}

The current dispatch method provides an object like:

{app:'myApp', 'tab': 'tab1', page: 'myApp', tabBar:'tabBar1'}

but I don't know the hierarchy. Is there some way I can create the hierarchy in the dispatch or pre-process

Regards
Jose

Use `lodash.merge`

We currently use Object spread operator ... to merge tracking contexts. This is a shallow merge, so it won't work for use cases where disparate components are providing different data to the same subkey of the context data.

We should be able to use lodash.merge to do the merging instead, which does a recursive deep merge on objects.

Remove default dispatch behavior

The current default dispatch behavior is specific to the User Hub team. For the majority of use cases, the dispatch function will probably be just a simple (data) => window.dataLayer.push(data) which we could eventually provide as the default or as an adapter that's optionally imported out of the box, but I think a good first step would be to remove this default behavior (it would also reduce the bundle size of this module).

cc @aneudy1702 @nicolehollyNYT

Fire track event when element becomes visible

First off, thanks for making this. I have been using react-tracking for a few months now and it's so clean while getting me the granularity of tracking I desire. Kudos.

I am curious how users of this library check for component visibility before firing an event. A simple use case is that I have a hidden alert on the page. I want to fire a "show" event when this alert becomes visible (not when it is mounted). This use case is pretty common -- think modals, buttons which are invisible on small screens but become visible on large screens, etc...

So far, the best I have come up with is to use an external library to track component visibility and combine that with react-tracking to fire events then the component becomes visible. But it's clunky. I am envisioning something like this instead:

@track(props => ({ button: props.id, event: 'button.show' }), { dispatchOnVisible: true })
class MyButton extends PureComponent {
  ...
}

Thoughts?

Suggestion: Add type-safe tracking helpers

Hello! We're using this library to integrate tracking into our React front-end, but we use TypeScript and trying to type the track function is a nightmare. The type definitions (@types/react-tracking) do not work as expected and we've resorted to wrapping track (typed as any) in several other functions to get some type safety. What we ended up with were:

  • TrackingProvider: Component to wrap the entire application
  • withTracking: HOF to augment component props with tracking.trackEvents
  • trackOnMount: HOF to dispatch a tracking event on mount (only accepts a function with props so that it works similar to how Apollo populates query arguments with prop values)
  • track: Method decorator (also only accepts a function with props)

Are you interested in moving toward a more static interface like this? Where one function can only do one thing? It would make integrating with TypeScript projects a lot easier. Of course, NYTimes may not have this problem though. LMK! ๐Ÿ™‚

One more note: Introducing TrackingProvider might be necessary anyway with the new context API?
ยฏ\_(ใƒ„)_/ยฏ

Decorator pre-requisites & sending data to analytics library

Hello,

Thanks for this module -- I've been doing some initial sandboxing in a basic create-react-app and have ran into two questions.

  1. I'm having issues adding tracking events via decorators. I tried incorporating the babel decorators plugin but I believe this requires using "npm run eject" and modifying core files. Just curious if you know of a simpler way to use the decorators? Otherwise, I've been using the method described in the Stateless Functional components for all files which works too.

  2. Can you help explain how to integrate this module with an analytics tool? For example, if I want each event sent to analytics with a POST to an API endpoint...where would that call go? I seem to be missing a step here because I only want to send the most recent item added to the dataLayer, not the entire dataLayer every time an event is triggered.

Thanks! #

Allow `options` to accept `dispatchImmediately: Bool`

Instead of the current convention of firing a pageDataReady event when there is a page property on the data object, it would be more flexible to accept a dispatchImmediately property on the options object (the second parameter that the decorator accepts) to dispatch an event immediately or not. This way we can generalize pageDataReady a bit since it's too specific to how an app works.

@track({ page: 'FooPage' }, { dispatchImmediately: true })
export default class FooPage extends Component { ... }

Open to better name ideas than "dispatchImmediately"

Adapt for External Analytics

Is there any plan to make the API viable for people to integrate with say Google Cloud, Google Analytics or an custom API? Currently in the stages of making an bundler that will act on tracking, and I think that this library could be super-fit for that, if one could export data from the tracking itself

What are your thoughts?

Handle sCU() and PureComponents better

Because of the way the React context API currently works, if somewhere in the component hierarchy there is a PureComponent or a component implements shouldComponentUpdate() then descendants from that point in the tree may not see context data correctly.

One way around this is to use a library like react-broadcast to reliably communicate context data throughout the tree. There might also be other ways? Open to suggestions!

Can't access reffed component

Hi guys,

We're having issues using react-tracking with reffed components.

The following example illustrates this:

@track({})
export default class ChildComponent extends Component {
    focus() {
        ...
    }
    ...
}

export default class ParentCompoent extends Component {
    componentDidMount() {
        this.child.focus()
    }
    render() {
        <ChildComponent ref={el => this.child = el}/>
    }
}

ParentComponent can't call ChildComponent's focus method since @track annotation wraps the component without allowing access to the component itself.

React-redux solves this with an option called withRef and and exposing a function called getWrappedInstance, would be great if could also see something similar here.

Thank you!

TypeScript typings

Hi!

I seem to recall that when we last spoke you were saying that you may be interested in hosting TypeScript type declarations inside this repository. Is that correct? If not, Iโ€™ll just move our typings to the DefinitelyTyped repo instead.

Support for react-router4 and multiple analytics accounts

Thanks for providing the module. Have two questions -

  1. Wondering if react-tracking supports react-router4? We wish to track page view events.
  2. Can module support use of multiple simultaneous analytics accounts (say different google analytics accounts)? Useful for sites that has multi-tenants each tracking their own analytics.

Module not found: Error: Can't resolve 'core-js/modules/es6.function.bind

I'm getting this error after upgrade to v5.7.0

Captura de pantalla de 2019-03-21 15-58-26

It is the only package that give me that error ๐Ÿค”
Maybe it is because I'm using core-js v3? ๐Ÿค”

"react-tracking": "^5.7.0"
"@babel/core": "^7.4.0"
"@babel/runtime-corejs3": "^7.4.2"
"core-js": "^3.0.0"
"regenerator-runtime": "^0.13.2"
"@babel/runtime": "^7.2.0"
"@babel/plugin-transform-runtime": "^7.4.0"

dispatch to data layer not referenced in window

Currently process() & dispatch() only take argument data. Does that imply the data layer must be referenced in window? e.g. window. dataLayer

My case is I'm using https://github.com/mobxjs/mobx-react where I would like to put the data layer inside props.rootStore. I don't see a way to have it available inside dispatch(), even the react component is a child of the StoreProvider. I tried to hack around with process() but seems the merge will make observers converted to plain object.

Is dispatch(data,props) feasible?

Better way to handle pageView events

Right now in order to attach "one-off" tracking data to handle something like a Page View event, requires the user to supply a dispatchOnMount() function. While this works, it becomes increasingly verbose as the number of pages grows. It would be nice to provide a top-level enrich() or process() (open to better names?) function that takes in the tracking data and dispatches an event. It can be defined right where dispatch() is defined so that it's handled in the same way everywhere.

This means, instead of having to define a dispatchOnMount() for every page like we do today:

@track({
  presentation: {
    pageType: 'Homepage',
  },
}, { dispatchOnMount: () => ({ event: 'pageDataReady' }) })
export default class Home extends Component { ... }

You can just define it on some top-level component once:

import track from 'nyt-react-tracking';
import dispatch from './GTMAdapter';

@track({}, {
  dispatch,
  process: data => {
    return data.presentation && data.presentation.pageType ? {
      event: 'pageDataReady'
    } : {}
  }
})
export default class App extends Component { ... }

And then any pages decorated with a data.presentation.pageType property would automatically dispatch { event: 'pageDataReady' }.

cc @aneudy1702 @jaredmcdonald

Expose decorated component

We're evaluating using this within our application and a talking point came up on testing components that are decorated with track. It would be great to have access to the decorated component so it can be tested without having to mock track.

react-dnd has a pretty cool pattern for this, exposing a DecoratedComponent static property: http://react-dnd.github.io/react-dnd/docs-testing.html (source: https://github.com/react-dnd/react-dnd/tree/master/packages/react-dnd/src/decorateHandler.tsx#L39).

If your team has any insight into how you test components decorated with track, I'm all ears.

Alternatively, if you would be open to an approach similar to react-dnd for exposing the decorated component, I'd be willing to open a PR for that!

Thanks for the library!

Default dispatch function does not appear to work as expected

I've attempted to setup a track decorator on a high level component which contains an option object as a second argument, like so:

import track from 'react-tracking'
...
@track({}, { process: (ownTrackingData) => {ownTrackingData.page ? {event: 'Pageview'} : null }} )
class AppWithAuth extends React.Component {
...

Based on the docs, I gathered that when a lower level track decorator dispatches an event, the default dispatch function pushes the event object onto the window.dataLayer array which is subsequently sent to Google Analytics. I am not seeing this behavior. window.dataLayer does not update when the event is fired.

However, if I replace the track decorator on the high level component with

@track({}, { process: (ownTrackingData) => {console.log(ownTrackingData);return ownTrackingData.page ? {event: 'Pageview'} : null }, dispatch: (d) => {console.log(d); (window.dataLayer = window.dataLayer || []).push(d)}} )

I am able to see the event objects pushed onto the window.dataLayer array through Chrome's javascript console. I also am able to see the Pageview event on my Google Analytics dashboard.

Decorate class method passed to child component

Thanks for your wonderful plugin.

After reading readMe, I wonder how to decorate class methods passed to childComponent as event handler with params.

For example. I get class component Wrapper

class Wrapper extends Component  {
    state: {
        tabIndex: 0,
         buttons: [1,2,3],
     }
    switchTab(index) {
        this.setState({ tabIndex: index })
    }
    render() {
       return <Tabs clickHandler={this.swtichTab.bind(this)} } />
    }
}

And it contains several buttons

({ buttons, switchTab}) => (
    <div>
        buttons.map((b, index) => <button onclick={() => { switchTab(index) }}>{b}</button>)
    </div>
)

Is there a way to track index param for decorated switchTab param?

Error when installing via Yarn

Looks like installing nyt-react-tracking via GitHub using Yarn produces some unexpected results: https://github.com/nytm/wf-project-vi/pull/1860#issuecomment-298986731

Specifically, I found that Yarn installs the src folder of the repo, which in turn causes Webpack to resolve files from src rather than build, leading to the following Webpack errors (copied from nytm/wf-project-vi#1860):

Webpack Error: "Unepxected token" in nyt-react-tracking
$ make dev 
node_modules/.bin/kyt dev

โ„น๏ธ  Using kyt config at /Users/206837/Sites/NYTM/wf-project-vi/kyt.config.js
โ„น๏ธ  What are you doing in your modifyWebpackConfig?
โ„น๏ธ  Let us know: https://github.com/NYTimes/kyt/issues/432

๐Ÿ”ฅ  Starting development build...

๐Ÿ‘  Client webpack configuration compiled
๐Ÿ‘  Server webpack configuration compiled
(node:54911) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
[BABEL] Note: The code generator has deoptimised the styling of "/Users/206837/Sites/NYTM/wf-project-vi/src/public/fonts.js" as it exceeds the max of "500KB".

โŒ  Client build failed

Hash: e24d4c8ad60729072d82
Version: webpack 2.2.1
Time: 38074ms
                                                                                Asset     Size  Chunks  Chunk Names
                        ../public/vi-assets/fonts-9b8d1c73f5aa5bdee04a89eca59817d5.js  2.98 MB          
            ../public/vi-assets/apple-touch-icon-bd3d976e9d23695765d0f6e5b19b4d27.png  4.24 kB          
                     ../public/vi-assets/favicon-730f775aebf7d74de62dc7b1a503e585.ico  20.2 kB          
../public/vi-assets/ios-default-homescreen-57x57-95b3f892ee611952b8574179d9feac20.png  1.62 kB          
            ../public/vi-assets/ios-ipad-144x144-bd3d976e9d23695765d0f6e5b19b4d27.png  4.24 kB          
          ../public/vi-assets/ios-iphone-114x144-a09dfadde0c831ad4fe57f8c738d15f7.png   3.3 kB          
                                                                              main.js  11.1 MB       0  main
chunk    {0} main.js (main) 4.29 MB [entry] [rendered]
    [1] ./~/react/react.js 56 bytes {0} [built]
   [42] (webpack)/buildin/global.js 509 bytes {0} [built]
  [143] ./~/react-dom/index.js 59 bytes {0} [built]
  [212] ./~/isomorphic-relay/lib/index.js 899 bytes {0} [built]
  [232] ./~/react-router/es/index.js 1.46 kB {0} [built]
  [649] ./src/client/index.js 6.04 kB {0} [built]
  [650] ./~/babel-polyfill/lib/index.js 833 bytes {0} [built]
  [651] ./~/react-hot-loader/patch.js 41 bytes {0} [built]
  [652] (webpack)-hot-middleware/client.js?reload=true&path=http://localhost:3001/__webpack_hmr 4.6 kB {0} [built]
 [1032] ./~/core-js/shim.js 7.38 kB {0} [built]
 [1089] ./~/isomorphic-relay-router/lib/index.js 795 bytes {0} [built]
 [1294] ./~/react-hot-loader/lib/patch.js 209 bytes {0} [built]
 [1387] ./~/regenerator-runtime/runtime.js 24.4 kB {0} [built]
 [1498] (webpack)-hot-middleware/client-overlay.js 1.74 kB {0} [built]
 [1502] multi babel-polyfill react-hot-loader/patch webpack-hot-middleware/client?reload=true&path=http://localhost:3001/__webpack_hmr ./src/client/index.js 64 bytes {0} [built]
     + 1488 hidden modules

ERROR in ./~/@nyt/nyt-react-tracking/src/withTrackingComponentDecorator.js
Module parse failed: /Users/206837/Sites/NYTM/wf-project-vi/node_modules/@nyt/nyt-react-tracking/src/withTrackingComponentDecorator.js Unexpected token (38:25)
You may need an appropriate loader to handle this file type.
|       }
| 
|       static displayName = `WithTracking(${decoratedComponentName})`;
|       static contextTypes = {
|         tracking: TrackingPropType,
 @ ./~/@nyt/nyt-react-tracking/src/index.js 1:0-93
 @ ./src/routes/NotFound/index.js
 @ ./src/routes/index.js
 @ ./src/client/index.js
 @ multi babel-polyfill react-hot-loader/patch webpack-hot-middleware/client?reload=true&path=http://localhost:3001/__webpack_hmr ./src/client/index.js

โ„น๏ธ  See webpack error above
webpack built e24d4c8ad60729072d82 in 38074ms

Our options:

  • Reconfigure Webpack in wf-project-vi to really only ever resolve modules from nyt-react-tracking's build folder
  • Wait for Yarn to resolve an issue in which it appears to ignore the package.json files prop for certain resolvers: yarnpkg/yarn#2822 (comment)
  • Some other third thing

Does react-tracking record impression?

Hello,

I found react-tracking while looking for a solution to track clicks and impressions on a react web application. After some reading it looks like react-tracking handles click events. I want to ask if this library also help record impression events (a part/component of a page shows up on the screen) ? If the library does support that where can I find docs and example ?

Thank you so much for your help, really appreciate it !

Fix missing peerDeps

We're missing some peerDeps as part of v5.5.0 (Babel 7 upgrade) in #94 ( cc @damassi )

MissingDependencyError
This package (or this version) uses `core-js/modules/es6.array.iterator`, 
`core-js/modules/es6.function.bind`, `core-js/modules/es6.function.name`, 
`core-js/modules/es6.object.assign`, `core-js/modules/es6.object.create`, 
`core-js/modules/es6.object.define-property`, `core-js/modules/es6.object.keys`, 
`core-js/modules/es6.object.set-prototype-of`, `core-js/modules/es6.promise`, 
`core-js/modules/es6.reflect.apply`, `core-js/modules/es6.reflect.define-property`, 
`core-js/modules/es6.symbol`, `core-js/modules/es7.symbol.async-iterator` and 
`core-js/modules/web.dom.iterable`, 
but does not specify them either as a dependency or a peer dependency

As reported by bundlephobia

Please add instructions how to test decorated methods

Example 1:

@track(props => {
  return { page: props.isNew ? 'new' : 'existing' };
})

The only way to test this decorator is to extract the callback function.

Example 2:
There is no way to jest.spyOn() on the decorated method or override a decorated method.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.