{"id":13686101,"url":"https://github.com/sumup-oss/collector","last_synced_at":"2025-05-01T05:31:40.157Z","repository":{"id":36965108,"uuid":"217528987","full_name":"sumup-oss/collector","owner":"sumup-oss","description":"DEPRECATED! A library of React components and hooks for contextual user-interaction tracking for complex interfaces with a predictable event schema","archived":true,"fork":false,"pushed_at":"2024-04-11T00:08:15.000Z","size":3338,"stargazers_count":20,"open_issues_count":0,"forks_count":2,"subscribers_count":12,"default_branch":"main","last_synced_at":"2025-03-26T14:54:36.320Z","etag":null,"topics":["event-tracking","react"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sumup-oss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-10-25T12:29:09.000Z","updated_at":"2024-05-06T14:27:33.000Z","dependencies_parsed_at":"2024-11-12T08:41:54.471Z","dependency_job_id":null,"html_url":"https://github.com/sumup-oss/collector","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumup-oss%2Fcollector","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumup-oss%2Fcollector/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumup-oss%2Fcollector/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sumup-oss%2Fcollector/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sumup-oss","download_url":"https://codeload.github.com/sumup-oss/collector/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251830449,"owners_count":21650802,"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":["event-tracking","react"],"created_at":"2024-08-02T14:01:06.317Z","updated_at":"2025-05-01T05:31:39.865Z","avatar_url":"https://github.com/sumup-oss.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"⚠️ **This package has been deprecated and will not receive further updates.** ⚠️\n\nWe recommend [walker.js](https://github.com/elbwalker/walker.js) as a framework-agnostic alternative.\n\n\u003cdiv align=\"center\"\u003e\n\n# Collector\n\nCollector is a library of React components and hooks that facilitates contextual user-interaction tracking for complex interfaces with a predictable event schema.\n\n[![Stars](https://img.shields.io/github/stars/sumup-oss/collector?style=social)](https://github.com/sumup-oss/collector/) [![Version](https://img.shields.io/npm/v/@sumup/collector)](https://www.npmjs.com/package/@sumup/collector) [![License](https://img.shields.io/badge/license-Apache%202-lightgrey.svg)](LICENSE)\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.1%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)\n\n\u003c/div\u003e\n\n## Table of Contents \u003c!-- omit in toc --\u003e\n\n- [Concepts](#concepts)\n  - [Problem Statement](#problem-statement)\n  - [Event Schema](#event-schema)\n  - [Page View](#page-view)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [TrackingRoot](#trackingroot)\n  - [TrackingView](#trackingview)\n  - [TrackingElement](#trackingelement)\n  - [useClickTrigger](#useclicktrigger)\n  - [useSectionExpandedTrigger](#usesectionexpandedtrigger)\n  - [useSubmitTrigger](#usesubmittrigger)\n  - [usePageViewTrigger](#usepageviewtrigger)\n- [Plugin](#plugin)\n  - [getFlushedPayload](#getflushedpayload)\n- [Code of Conduct (CoC)](#code-of-conduct-coc)\n  - [Maintainers](#maintainers)\n- [About SumUp](#about-sumup)\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eTL;DR\u003c/strong\u003e\u003c/summary\u003e\n\n```jsx\nimport React from 'react';\nimport {\n  TrackingRoot,\n  TrackingView,\n  TrackingElement,\n  useClickTrigger,\n} from '@sumup/collector';\n\nfunction Button({ onClick, 'tracking-label': trackingId, children }) {\n  const dispatch = useClickTrigger();\n  const handleClick = (event) =\u003e {\n    if (trackingId) {\n      dispatch({ label: trackingId, component: 'button' });\n    }\n    if (onClick) {\n      onClick(event);\n    }\n  };\n\n  return \u003cbutton onClick={handleClick}\u003e{children}\u003c/button\u003e;\n}\n\nfunction App() {\n  return (\n    \u003cTrackingRoot\n      name=\"my-app\"\n      onDispatch={(event) =\u003e {\n        console.log(event);\n      }}\n    \u003e\n      \u003cTrackingView name=\"page\"\u003e\n        \u003cTrackingElement name=\"element-a\"\u003e\n          \u003cButton tracking-label=\"show-content-a\"\u003eClick me\u003c/Button\u003e\n        \u003c/TrackingElement\u003e\n\n        \u003cTrackingElement name=\"element-b\"\u003e\n          \u003cButton tracking-label=\"show-content-b\"\u003eClick me\u003c/Button\u003e\n        \u003c/TrackingElement\u003e\n      \u003c/TrackingView\u003e\n    \u003c/TrackingRoot\u003e\n  );\n}\n```\n\n\u003c/details\u003e\n\n## Concepts\n\n### Problem Statement\n\nHigh-quality event tracking data requires contextual information. When a user interacts with your application, for example by clicking a button, it is useful to know where this button is located in the page hierarchy to put the event in context. The larger a web applications grows, the harder it becomes to provide predictable and traceable tracking structures.\n\nA full example of these challenges is outlined in the [motivation](https://github.com/sumup-oss/collector/blob/main/MOTIVATION.md) document.\n\nCollector was built to track user-interactions with contextual information and high granularity. Using an agnostic event schema you can serve different tracking purposes with it.\n\n### Event Schema\n\nCollector's philosophy is to structure your events based on your UI hierarchy. When dispatching events this way, it's easier to reason about the event payload. Based on this image we can start discussing about the event schema:\n\n\u003cdiv align=\"center\"\u003e\n\n![Collector's Concept](https://user-images.githubusercontent.com/2780941/90146083-ee319280-dd80-11ea-88fe-a940dc4b695e.png)\n\n\u003c/div\u003e\n\nIn order to support the app/view/elements hierarchy, the event schema is defined by the following keys:\n\n```ts\ninterface Event {\n  app: string; // The application name\n  view: string; // The current \"view\". Can be overwritten\n  elementTree: string[]; // The current list of rendered \u003cTrackingElement /\u003e down to the dispatched event\n  component?: 'button' | 'link'; // Which primitive dispatched the event\n  label?: string;\n  event:\n    | 'click'\n    | 'view'\n    | 'load'\n    | 'page-view'\n    | 'page-reactivated'\n    | 'submit'\n    | 'browser-back'\n    | 'section-expanded'; // This property is added internally based on the kind of event you dispatched.\n  timestamp: number; // This property is added internally when the dispatch function is called\n  customParameters?: {\n    [key: string]: any;\n  };\n}\n```\n\nThe directives (`TrackingRoot = app`, `TrackingView = view` and `TrackingElement = elementTree`) are responsible for defining their respective attributes for the data structure. Whenever you dispatch an event, these values will be retrieved based on the component hierarchy, for example:\n\n```jsx\n \u003cTrackingRoot name=\"my-app\" onDispatch={console.log}\u003e\n   \u003cTrackingView name=\"account\"\u003e\n    ...\n    \u003cTrackingElement name=\"change-account-form\"\u003e\n     ...\n      \u003cTrackingElement name=\"validate-bank-account\"\u003e\n      ...\n      \u003c/TrackingElement\u003e\n    \u003c/TrackingElement\u003e\n   \u003c/TrackingView\u003e\n \u003cTrackingRoot\u003e\n```\n\nWould yield the following structure: `{ app: 'my-app', view: 'account', elementTree: ['change-account-form', 'validate-bank-account'] }`.\n\n### Page View\n\nTraditionally a \"page view\" is defined as \"an instance of a page being loaded (or reloaded) in a browser\" (from [Google](https://support.google.com/analytics/answer/6086080?hl=en) for Google Analytics). With single page applications (SPAs) internally navigating from one page to another page will not lead to a full page load, as the content needed to display a new page is dynamically inserted. Thus Collector's definition of a \"page view\" includes these additional scenarios.\n\nThe following rule set describes the most common events that trigger page views:\n\n- The page is initially loaded (or reloaded) in the browser (a full page load takes place) and active (in focus).\n- A significant visual change of the page has taken place, such as:\n  - An overlying modal, visually blocking (and deactivating) the underlying content has appeared (e.g. registration / login modals, cookie notifications, or product information modals).\n  - Inversely, when the pages underlying content becomes visible / active again, after a modal was closed.\n  - The main contents of a page have changed due to filtering or searching on that page (e.g. a product list is filtered or ordered by the lowest price).\n- A new page component has been mounted (after the initial page load), leading to a route change and the route change is completed (i.e. the path of the URL has changed).\n- A browser window / tab displaying a page is activated (in focus) after being inactive (blurred).\n\n## Installation\n\nCollector needs to be installed as a dependency via the [Yarn](https://yarnpkg.com) or [npm](https://www.npmjs.com) package managers. The npm CLI ships with [Node](https://nodejs.org/en/). You can read how to install the Yarn CLI in [their documentation](https://yarnpkg.com/en/docs/install).\n\nDepending on your preference, run one of the following.\n\n```sh\n# With Yarn\n$ yarn add @sumup/collector\n\n# With npm\n$ npm install @sumup/collector\n```\n\nCollector requires [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) v16.8+ as peer dependencies.\n\n## Usage\n\n### TrackingRoot\n\nThe TrackingRoot is responsible for storing the `app` value and the `dispatch` function. It is recommended to have only one TrackingRoot per application.\n\n```jsx\nimport React from 'react';\nimport { TrackingRoot } from '@sumup/collector';\n\nfunction App() {\n const handleDispatch = React.useCallback((event) =\u003e {\n   // You can define multipler handlers and transform the base event to support different schemas.\n   window.dataLayer.push(event)\n }, []);\n\n return (\n   \u003cTrackingRoot name=\"app\" onDispatch={handleDispatch}\u003e\n     ...\n   \u003cTrackingRoot\u003e\n );\n}\n```\n\nTo avoid unnecessary renders, we recommend providing `onDispatch` as a memoized function.\n\n\u003e The above code snippet demonstrates how to push events to the [Google Tag Manager dataLayer](https://support.google.com/tagmanager/answer/6164391?hl=en). This is just an example, Collector is agnostic of the tag management or analytics solution you use. In fact it's not even tied to analytics, you could just as well send the data to a structured logging service or anywhere else.\n\n### TrackingView\n\nThe TrackingView is responsible for storing the `view` value. It is recommended to have one TrackingView per [\"page view\"](#page-view).\n\n```jsx\nimport React from 'react';\nimport { TrackingView } from '@sumup/collector';\n\nfunction App() {\n return (\n   ...\n   \u003cTrackingView name=\"account\"\u003e\n     ...\n   \u003cTrackingView\u003e\n );\n}\n```\n\n### TrackingElement\n\nThe TrackingElement is responsible for storing the current `element` value. Elements are usually a representation of a feature/organism in your application such as a form.\n\n```jsx\nimport React from 'react';\nimport { TrackingElement } from '@sumup/collector';\n\nfunction App() {\n return (\n   ...\n   \u003cTrackingElement name=\"change-account-form\"\u003e\n     ...\n     \u003cTrackingElement name=\"forgot-password\"\u003e\n      ...\n     \u003c/TrackingElement\u003e\n   \u003cTrackingElement\u003e\n );\n}\n```\n\n### useClickTrigger\n\n`useClickTrigger` provides you a dispatch function for any kind of click event.\n\nThe dispatch function accepts the following interface:\n\n```jsx\ninterface Options {\n  component?: string;\n  label?: string;\n  customParameters?: {\n    [key: string]: any,\n  };\n  event: 'click'; // Added internally by the hook\n  timestamp: number; // Added internally when the dispatch function is called\n}\n```\n\n```jsx\nimport React from 'react';\nimport { useClickTrigger } from '@sumup/collector';\n\nfunction Button({ onClick, 'tracking-label': label, children }) {\n  const dispatch = useClickTrigger();\n  let handler = onClick;\n\n  if (label) {\n    handler = (e) =\u003e {\n      dispatch({ label, component: 'button' });\n      onClick \u0026\u0026 onClick(e);\n    };\n  }\n\n  return \u003cbutton onClick={handler}\u003e{children}\u003c/button\u003e;\n}\n```\n\n### useSectionExpandedTrigger\n\n`useSectionExpandedTrigger` provides you a dispatch function for a section expanded event.\n\nThe dispatch function accepts the following interface:\n\n```jsx\ninterface Options {\n  component?: string;\n  label?: string;\n  customParameters?: {\n    [key: string]: any,\n  };\n  event: 'section-expanded'; // Added internally by the hook\n  timestamp: number; // Added internally when the dispatch function is called\n}\n```\n\n```jsx\nimport React from 'react';\nimport { useSectionExpandedTrigger } from '@sumup/collector';\n\nfunction Section({ onClick, 'tracking-label': label, children }) {\n  const dispatch = useSectionExpandedTrigger();\n  let expandHandler = onClick;\n\n  if (label) {\n    expandHandler = (e) =\u003e {\n      dispatch({ label, component: 'section' });\n      onClick \u0026\u0026 onClick(e);\n    };\n  }\n\n  return \u003cdiv onClick={expandHandler}\u003e{children}\u003c/div\u003e;\n}\n```\n\n### useSubmitTrigger\n\n`useSubmitTrigger` provides you a dispatch function for any kind of form submission event.\n\nThe dispatch function accepts the following interface:\n\n```jsx\ninterface Options {\n  component?: string;\n  label?: string;\n  customParameters?: {\n    [key: string]: any,\n  };\n  event: 'submit'; // Added internally by the hook\n  timestamp: number; // Added internally when the dispatch function is called\n}\n```\n\n```jsx\nimport React from 'react';\nimport { useSubmitTrigger } from '@sumup/collector';\n\nfunction Form({ children }) {\n  const dispatch = useSubmitTrigger();\n\n  const submitHandler = (e) =\u003e {\n    e.preventDefault();\n\n    dispatch({ component: 'form' });\n  };\n\n  return \u003cform onSubmit={handler}\u003e{children}\u003c/form\u003e;\n}\n```\n\n### usePageViewTrigger\n\n`usePageViewTrigger()` lets you dispatch a [page view](#page-view) event.\n\nThe `pageView` event will be dispatched with:\n\n```ts\ninterface Event {\n  app: string;\n  view: string;\n  customParameters?: {\n    [key: string]: any;\n  };\n  event: 'page-view'; // Added internally by the hook\n  timestamp: number; // Added internally by the library when the dispatch function is called\n}\n```\n\nIn order to have a meaningful page view event, we recommend integrating the available hooks for page view after declaring the [TrackingRoot](#trackingroot) in your application.\n\nYou don't need to declare it after the [TrackingView](#trackingview) since any `TrackingView` component will overwrite the context value.\n\n```jsx\nimport React from 'react';\nimport {\n  TrackingRoot,\n  TrackingView,\n  usePageViewTrigger,\n} from '@sumup/collector';\n\ninterface Props {\n  children: React.ReactNode;\n  location: string;\n}\n\n// This could be a hook instead\nfunction PageView({ location, children }: Props) {\n  const dispatchPageView = usePageViewTrigger();\n\n  // run the effect everytime location changes\n  useEffect(() =\u003e {\n    dispatchPageView();\n  }, [location]);\n\n  return children;\n}\n```\n\n`usePageActiveTrigger` **automatically** dispatches an event whenever the tab becomes active again after being inactive (via [Visibility change](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event)). This is meant to be used whenever you want to track if people are changing tabs.\n\nKeep in mind only one \"pageActive\" trigger is required since it's a document event listener.\n\n```jsx\nimport React from 'react';\nimport { usePageActiveTrigger } from '@sumup/collector';\n\ninterface Props {\n  children: React.ReactNode;\n  location: string;\n}\n\nfunction PageActive({ location, children }: Props) {\n  usePageActiveTrigger();\n\n  return children;\n}\n```\n\n## Plugin\n\nHelpers for specific issue.\n\n### getFlushedPayLoad\n\nIf you are using Google Tag Manager(GTM) as your dispatch consumer, there is a known behaviour that GTM persists variables until they got flushed. For a non-nested event, a fixed schema with default undefined value flushes unused variable thus they don't pollute states for the next event. For a designed nested variable, eg, `customParameters` in Collector, a nested flush helps to keep states clean. In this plugin, an aggregated custom parameters based on payload history will be set as undefined and flushed by GTM.\n\nYou can find an example code here.\n\n```jsx\nimport React from 'react';\nimport { getFlushedPayload } from '@sumup/collector';\n\nfunction App() {\n const handleDispatch = React.useCallback((event) =\u003e {\n   // getFlushedPayload return a new event with flushed payload\n   const flushedEvent = getFlushedPayload(window.dataLayer, event);\n   window.dataLayer.push(flushedEvent)\n }, []);\n\n return (\n   \u003cTrackingRoot name=\"app\" onDispatch={handleDispatch}\u003e\n     ...\n   \u003cTrackingRoot\u003e\n );\n}\n```\n\n## Code of Conduct (CoC)\n\nWe want to foster an inclusive and friendly community around our Open Source efforts. Like all SumUp Open Source projects, this project follows the Contributor Covenant Code of Conduct. Please, [read it and follow it](CODE_OF_CONDUCT.md).\n\nIf you feel another member of the community violated our CoC or you are experiencing problems participating in our community because of another individual's behavior, please get in touch with our maintainers. We will enforce the CoC.\n\n### Maintainers\n\n- [Shih Yen Hwang](mailto:shih.yen.hwang@sumup.com)\n- [Connor Bär](mailto:connor.baer@sumup.com)\n\n## About SumUp\n\n![SumUp logo](https://raw.githubusercontent.com/sumup-oss/assets/main/sumup-logo.svg?sanitize=true)\n\n[SumUp](https://sumup.com) is a mobile-point of sale provider. It is our mission to make easy and fast card payments a reality across the _entire_ world. You can pay with SumUp in more than 30 countries, already. Our engineers work in Berlin, Cologne, Sofia, and Sāo Paulo. They write code in JavaScript, Swift, Ruby, Go, Java, Erlang, Elixir, and more.\n\nWant to come work with us? [Head to our careers page](https://sumup.com/careers) to find out more.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsumup-oss%2Fcollector","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsumup-oss%2Fcollector","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsumup-oss%2Fcollector/lists"}