{"id":13606142,"url":"https://github.com/nytimes/react-tracking","last_synced_at":"2025-04-10T23:22:10.664Z","repository":{"id":20951383,"uuid":"76411183","full_name":"nytimes/react-tracking","owner":"nytimes","description":"🎯 Declarative tracking for React apps.","archived":false,"fork":false,"pushed_at":"2023-10-18T10:28:18.000Z","size":2738,"stargazers_count":1888,"open_issues_count":38,"forks_count":125,"subscribers_count":28,"default_branch":"main","last_synced_at":"2025-04-10T10:46:27.282Z","etag":null,"topics":["decorator","hacktoberfest","react","track","tracking"],"latest_commit_sha":null,"homepage":"https://open.nytimes.com/introducing-react-tracking-declarative-tracking-for-react-apps-2c76706bb79a","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nytimes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2016-12-14T00:50:54.000Z","updated_at":"2025-04-08T10:56:19.000Z","dependencies_parsed_at":"2023-01-13T21:13:53.273Z","dependency_job_id":"949b0ae5-5359-487a-9d46-1b989738cb23","html_url":"https://github.com/nytimes/react-tracking","commit_stats":{"total_commits":173,"total_committers":36,"mean_commits":4.805555555555555,"dds":0.6647398843930636,"last_synced_commit":"c4b17ff033ec519824308062622017fa16b768f9"},"previous_names":[],"tags_count":56,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nytimes%2Freact-tracking","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nytimes%2Freact-tracking/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nytimes%2Freact-tracking/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nytimes%2Freact-tracking/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nytimes","download_url":"https://codeload.github.com/nytimes/react-tracking/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248313181,"owners_count":21082815,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decorator","hacktoberfest","react","track","tracking"],"created_at":"2024-08-01T19:01:06.468Z","updated_at":"2025-04-10T23:22:10.638Z","avatar_url":"https://github.com/nytimes.png","language":"JavaScript","funding_links":[],"categories":["JavaScript","📦 Legacy \u0026 Inactive Projects"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\u003cimg src=\"https://cdn-images-1.medium.com/max/1600/1*DKS5pYfsAz-H45myvnWWVw.gif\" style=\"max-width:75%;\"\u003e\u003c/p\u003e\n\n# react-tracking [![npm version](https://badge.fury.io/js/react-tracking.svg)](https://badge.fury.io/js/react-tracking)\n\n- React specific tracking library, usable as a higher-order component (as `@decorator` or directly), or as a React Hook\n- Compartmentalize tracking concerns to individual components, avoid leaking across the entire app\n- Expressive and declarative (in addition to imperative) API to add tracking to any React app\n- Analytics platform agnostic\n\nRead more in the [Times Open blog post](https://open.nytimes.com/introducing-react-tracking-declarative-tracking-for-react-apps-2c76706bb79a).\n\nIf you just want a quick sandbox to play around with:\n\n[![Edit react-tracking example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/reacttracking-example-qk30j4x1zj)\n\n## Installation\n\n```\nnpm install --save react-tracking\n```\n\n## Usage\n\n```js\nimport track, { useTracking } from 'react-tracking';\n```\n\nBoth `@track()` and `useTracking()` expect two arguments, `trackingData` and `options`.\n\n- `trackingData` represents the data to be tracked (or a function returning that data)\n- `options` is an optional object that accepts the following properties (when decorating/wrapping a component, it also accepts a `forwardRef` property):\n  - `dispatch`, which is a function to use instead of the default dispatch behavior. See the section on custom `dispatch()` [below](https://github.com/nytimes/react-tracking#custom-optionsdispatch-for-tracking-data).\n  - `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.\n  - `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](https://github.com/nytimes/react-tracking#top-level-optionsprocess).\n  - `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`.\n  - `mergeOptions` optionally provide deepmerge options, check [deepmerge options API](https://github.com/TehShrike/deepmerge#options) for details.\n\n#### `tracking` prop\n\nThe `@track()` decorator will expose a `tracking` prop on the component it wraps, that looks like:\n\n```js\n{\n  // tracking prop provided by @track()\n  tracking: PropTypes.shape({\n    // function to call to dispatch tracking events\n    trackEvent: PropTypes.func,\n\n    // function to call to grab contextual tracking data\n    getTrackingData: PropTypes.func,\n  });\n}\n```\n\nThe `useTracking` hook returns an object with this same shape, plus a `\u003cTrack /\u003e` component that you use to wrap your returned markup to pass contextual data to child components.\n\n### Usage with React Hooks\n\nWe can access the `trackEvent` method via the `useTracking` hook from anywhere in the tree:\n\n```js\nimport { useTracking } from 'react-tracking';\n\nconst FooPage = () =\u003e {\n  const { Track, trackEvent } = useTracking({ page: 'FooPage' });\n\n  return (\n    \u003cTrack\u003e\n      \u003cdiv\n        onClick={() =\u003e {\n          trackEvent({ action: 'click' });\n        }}\n      /\u003e\n    \u003c/Track\u003e\n  );\n};\n```\n\nThe `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](https://github.com/nytimes/react-tracking#usage-as-a-decorator)). It also returns an additional property on that object: a `\u003cTrack /\u003e` component that can be returned as the root of your component's sub-tree to pass any new contextual data to its children.\n\n\u003e Note that in most cases you would wrap the markup returned by your component with `\u003cTrack /\u003e`. 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 `\u003cTrack /\u003e` is if you're on some leaf component and don't have any more child components that need tracking info.\n\n```js\nimport { useTracking } from 'react-tracking';\n\nconst Child = () =\u003e {\n  const { trackEvent } = useTracking();\n\n  return (\n    \u003cdiv\n      onClick={() =\u003e {\n        trackEvent({ action: 'childClick' });\n      }}\n    /\u003e\n  );\n};\n\nconst FooPage = () =\u003e {\n  const { Track, trackEvent } = useTracking({ page: 'FooPage' });\n\n  return (\n    \u003cTrack\u003e\n      \u003cChild /\u003e\n      \u003cdiv\n        onClick={() =\u003e {\n          trackEvent({ action: 'click' });\n        }}\n      /\u003e\n    \u003c/Track\u003e\n  );\n};\n```\n\nIn the example above, the click event in the `FooPage` component will dispatch the following data:\n\n```\n{\n  page: 'FooPage',\n  action: 'click',\n}\n```\n\nBecause we wrapped the sub-tree returned by `FooPage` in `\u003cTrack /\u003e`, the click event in the `Child` component will dispatch:\n\n```\n{\n  page: 'FooPage',\n  action: 'childClick',\n}\n```\n\n### Usage as a Decorator\n\nThe default `track()` export is best used as a `@decorator()` using the [babel decorators plugin](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy).\n\nThe 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.\n\n_**Note:** In order to decorate class property methods within a class, as shown in the example below, you will need to enable [loose mode](https://babeljs.io/docs/en/babel-plugin-proposal-class-properties#loose) in the [babel class properties plugin](https://babeljs.io/docs/en/babel-plugin-proposal-class-properties)._\n\n```js\nimport React from 'react';\nimport track from 'react-tracking';\n\n@track({ page: 'FooPage' })\nexport default class FooPage extends React.Component {\n  @track({ action: 'click' })\n  handleClick = () =\u003e {\n    // ... other stuff\n  };\n\n  render() {\n    return \u003cbutton onClick={this.handleClick}\u003eClick Me!\u003c/button\u003e;\n  }\n}\n```\n\n### Usage on Stateless Functional Components\n\nYou 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:\n\n```js\nimport track from 'react-tracking';\n\nconst FooPage = props =\u003e {\n  return (\n    \u003cdiv\n      onClick={() =\u003e {\n        props.tracking.trackEvent({ action: 'click' });\n\n        // ... other stuff\n      }}\n    /\u003e\n  );\n};\n\nexport default track({\n  page: 'FooPage',\n})(FooPage);\n```\n\n_This is also how you would use this module without `@decorator` syntax, although this is obviously awkward and the decorator syntax is recommended._\n\n### Custom `options.dispatch()` for tracking data\n\nBy default, data tracking objects are pushed to `window.dataLayer[]` (see [src/dispatchTrackingEvent.js](src/dispatchTrackingEvent.js)). This is a good default if you use Google Tag Manager.\nHowever, please note that in React Native environments, the window object is undefined as it's specific to web browser environments.\nYou 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).\n\nFor example, to push objects to `window.myCustomDataLayer[]` instead, you would decorate your top-level `\u003cApp /\u003e` component like this:\n\n```js\nimport React, { Component } from 'react';\nimport track from 'react-tracking';\n\n@track({}, { dispatch: data =\u003e window.myCustomDataLayer.push(data) })\nexport default class App extends Component {\n  render() {\n    return this.props.children;\n  }\n}\n```\n\nThis can also be done in a functional component using the `useTracking` hook:\n\n```js\nimport React from 'react';\nimport { useTracking } from 'react-tracking';\n\nexport default function App({ children }) {\n  const { Track } = useTracking(\n    {},\n    { dispatch: data =\u003e window.myCustomDataLayer.push(data) }\n  );\n\n  return \u003cTrack\u003e{children}\u003c/Track\u003e;\n}\n```\n\nNOTE: 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.\n\n### When to use `options.dispatchOnMount`\n\nYou 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:\n\n#### Using `options.dispatchOnMount` as a boolean\n\nTo 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.\n\n```js\n@track({ page: 'FooPage' }, { dispatchOnMount: true })\nclass FooPage extends Component { ... }\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eExample using hooks\u003c/summary\u003e\n\n```js\nfunction FooPage() {\n  useTracking({ page: 'FooPage' }, { dispatchOnMount: true });\n}\n```\n\n\u003c/details\u003e\n\nWill dispatch the following data (assuming no other tracking data in context from the rest of the app):\n\n```\n{\n  page: 'FooPage'\n}\n```\n\nOf 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.\n\n_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._\n\n#### Using `options.dispatchOnMount` as a function\n\nIf 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.\n\nA use case for this would be that you want to provide extra tracking data without adding it to the context.\n\n```js\n@track({ page: 'FooPage' }, { dispatchOnMount: (contextData) =\u003e ({ event: 'pageDataReady' }) })\nclass FooPage extends Component { ... }\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eExample using hooks\u003c/summary\u003e\n\n```js\nfunction FooPage() {\n  useTracking(\n    { page: 'FooPage' },\n    { dispatchOnMount: contextData =\u003e ({ event: 'pageDataReady' }) }\n  );\n}\n```\n\n\u003c/details\u003e\n\nWill dispatch the following data (assuming no other tracking data in context from the rest of the app):\n\n```\n{\n  event: 'pageDataReady',\n  page: 'FooPage'\n}\n```\n\n### Top level `options.process`\n\nWhen 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.\n\nA 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`:\n\n```js\n@track({}, { process: (ownTrackingData) =\u003e ownTrackingData.page ? { event: 'pageview' } : null })\nclass App extends Component {...}\n\n...\n\n@track({ page: 'Page1' })\nclass Page1 extends Component {...}\n\n@track({})\nclass Page2 extends Component {...}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eExample using hooks\u003c/summary\u003e\n\n```js\nfunction App() {\n  const { Track } = useTracking(\n    {},\n    {\n      process: ownTrackingData =\u003e\n        ownTrackingData.page ? { event: 'pageview' } : null,\n    }\n  );\n\n  return (\n    \u003cTrack\u003e\n      \u003cPage1 /\u003e\n      \u003cPage2 /\u003e\n    \u003c/Track\u003e\n  );\n}\n\nfunction Page1() {\n  useTracking({ page: 'Page1' });\n}\n\nfunction Page2() {\n  useTracking({});\n}\n```\n\n\u003c/details\u003e\n\nWhen `Page1` mounts, event with data `{page: 'Page1', event: 'pageview'}` will be dispatched.\nWhen `Page2` mounts, nothing will be dispatched.\n\n_**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](https://codesandbox.io/s/flamboyant-keldysh-g3xt9?file=/src/App.tsx) for an example). Follow [issue #189](https://github.com/nytimes/react-tracking/issues/189) to monitor progress on a fix._\n\n### Tracking Asynchronous Methods\n\nAsynchronous 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.\n\n```js\n// ...\n  @track()\n  async handleEvent() {\n    return await asyncCall(); // returns a promise\n  }\n// ...\n```\n\nOr without async/await syntax:\n\n```js\n// ...\n  @track()\n  handleEvent() {\n    return asyncCall(); // returns a promise\n  }\n```\n\n### Advanced Usage\n\nYou 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:\n\n```js\nimport React from 'react';\nimport track from 'react-tracking';\n\n// In this case, the \"page\" tracking data\n// is a function of one of its props (isNew)\n@track(props =\u003e {\n  return { page: props.isNew ? 'new' : 'existing' };\n})\nexport default class FooButton extends React.Component {\n  // In this case the tracking data depends on\n  // some unknown (until runtime) value\n  @track((props, state, [event]) =\u003e ({\n    action: 'click',\n    label: event.currentTarget.title || event.currentTarget.textContent,\n  }))\n  handleClick = event =\u003e {\n    if (this.props.onClick) {\n      this.props.onClick(event);\n    }\n  };\n\n  render() {\n    return \u003cbutton onClick={this.handleClick}\u003e{this.props.children}\u003c/button\u003e;\n  }\n}\n```\n\nNOTE: That the above code utilizes some of the newer ES6 syntax. This is what it would look like in ES5:\n\n```js\n// ...\n  @track(function(props, state, args) {\n    const event = args[0];\n    return {\n      action: 'click',\n      label: event.currentTarget.title || event.currentTarget.textContent\n    };\n  })\n// ...\n```\n\nWhen 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:\n\n```js\n// ...\n  @track((props, state, methodArgs, [{ value }, err]) =\u003e {\n    if (err) { // promise was rejected\n      return {\n        label: 'async action',\n        status: 'error',\n        value: err\n      };\n    }\n    return {\n      label: 'async action',\n      status: 'success',\n      value // value is \"test\"\n    };\n  })\n  handleAsyncAction(data) {\n    // ...\n    return Promise.resolve({ value: 'test' });\n  }\n// ...\n```\n\nIf the function returns a falsy value (e.g. `false`, `null` or `undefined`) then the tracking call will not be made.\n\n### Accessing data stored in the component's `props` and `state`\n\nFurther runtime data, such as the component's `props` and `state`, are available as follows:\n\n```js\n  @track((props, state) =\u003e ({\n    action: state.following ? \"unfollow clicked\" : \"follow clicked\",\n    name: props.name\n  }))\n  handleFollow = () =\u003e {\n     this.setState({ following: !this.state.following })\n    }\n  }\n```\n\n#### Example `props.tracking.getTrackingData()` usage\n\nAny 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.\n\n```js\nimport React from 'react';\nimport track from 'react-tracking';\n\n// Pass a function to the decorator\n@track(() =\u003e {\n  const randomId = Math.floor(Math.random() * 100);\n\n  return {\n    page_view_id: randomId,\n  };\n})\nexport default class AdComponent extends React.Component {\n  render() {\n    const { page_view_id } = this.props.tracking.getTrackingData();\n\n    return \u003cAd pageViewId={page_view_id} /\u003e;\n  }\n}\n```\n\nNote 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:\n\n```js\nimport React, { useMemo } from 'react';\nimport { useTracking } from 'react-tracking';\n\nexport default function AdComponent() {\n  const randomId = useMemo(() =\u003e Math.floor(Math.random() * 100), []);\n  const { getTrackingData } = useTracking({ page_view_id: randomId });\n  const { page_view_id } = getTrackingData();\n\n  return \u003cAd pageViewId={page_view_id} /\u003e;\n}\n```\n\n### Tracking Data\n\nNote that there are no restrictions on the objects that are passed in to the decorator or hook.\n\n**The format for the tracking data object is a contract between your app and the ultimate consumer of the tracking data.**\n\nThis 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).\n\n### TypeScript Support\n\nYou 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](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-tracking/test/react-tracking-with-types-tests.tsx).\n\n### PropType Support\n\nThe `props.tracking` PropType is exported for use, if desired:\n\n```js\nimport { TrackingPropType } from 'react-tracking';\n```\n\nAlternatively, if you want to just silence proptype errors when using [eslint react/prop-types](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prop-types.md), you can add this to your eslintrc:\n\n```json\n{\n  \"rules\": {\n    \"react/prop-types\": [\"error\", { \"ignore\": [\"tracking\"] }]\n  }\n}\n```\n\n### Deepmerge\n\nThe 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: https://github.com/nytimes/react-tracking/issues/186\n\nYou can also use/reference the copy of deepmerge that react-tracking uses, as it's re-exported for convenience:\n\n```js\nimport { deepmerge } from 'react-tracking';\n```\n\n### Old Browsers Support\n\nGoing forward from version `9.x`, we do not bundle `core-js` (ES6 polyfills) anymore. To support old browsers, please add [`core-js`](https://github.com/zloirock/core-js) to your project.\n\n[deepmerge]: https://www.npmjs.com/package/deepmerge\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnytimes%2Freact-tracking","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnytimes%2Freact-tracking","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnytimes%2Freact-tracking/lists"}