@sumup/collector

Collector is a library of React components and hooks that facilitates contextual user-interaction tracking for complex interfaces with a predictable event schema.

Usage no npm install needed!

<script type="module">
  import sumupCollector from 'https://cdn.skypack.dev/@sumup/collector';
</script>

README

Collector

Collector is a library of React components and hooks that facilitates contextual user-interaction tracking for complex interfaces with a predictable event schema.

Stars Version License Contributor Covenant

Table of Contents

TL;DR
import React from 'react';
import {
  TrackingRoot,
  TrackingView,
  TrackingElement,
  useClickTrigger,
} from '@sumup/collector';

function Button({ onClick, 'tracking-label': trackingId, children }) {
  const dispatch = useClickTrigger();
  const handleClick = (event) => {
    if (trackingId) {
      dispatch({ label: trackingId, component: 'button' });
    }
    if (onClick) {
      onClick(event);
    }
  };

  return <button onClick={handleClick}>{children}</button>;
}

function App() {
  return (
    <TrackingRoot
      name="my-app"
      onDispatch={(event) => {
        console.log(event);
      }}
    >
      <TrackingView name="page">
        <TrackingElement name="element-a">
          <Button tracking-label="show-content-a">Click me</Button>
        </TrackingElement>

        <TrackingElement name="element-b">
          <Button tracking-label="show-content-b">Click me</Button>
        </TrackingElement>
      </TrackingView>
    </TrackingRoot>
  );
}

Concepts

Problem Statement

High-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.

A full example of these challenges is outlined in the motivation document.

Collector 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.

Event Schema

Collector'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:

Collector's Concept

In order to support the app/view/elements hierarchy, the event schema is defined by the following keys:

interface Event {
  app: string; // The application name
  view: string; // The current "view". Can be overwritten
  elementTree: string[]; // The current list of rendered <TrackingElement /> down to the dispatched event
  component?: 'button' | 'link'; // Which primitive dispatched the event
  label?: string;
  event:
    | 'click'
    | 'view'
    | 'load'
    | 'page-view'
    | 'page-reactivated'
    | 'submit'
    | 'browser-back'
    | 'section-expanded'; // This property is added internally based on the kind of event you dispatched.
  timestamp: number; // This property is added internally when the dispatch function is called
  customParameters?: {
    [key: string]: any;
  };
}

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

 <TrackingRoot name="my-app" onDispatch={console.log}>
   <TrackingView name="account">
    ...
    <TrackingElement name="change-account-form">
     ...
      <TrackingElement name="validate-bank-account">
      ...
      </TrackingElement>
    </TrackingElement>
   </TrackingView>
 <TrackingRoot>

Would yield the following structure: { app: 'my-app', view: 'account', elementTree: ['change-account-form', 'validate-bank-account'] }.

Page View

Traditionally a "page view" is defined as "an instance of a page being loaded (or reloaded) in a browser" (from Google 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.

The following rule set describes the most common events that trigger page views:

  • The page is initially loaded (or reloaded) in the browser (a full page load takes place) and active (in focus).
  • A significant visual change of the page has taken place, such as:
    • An overlying modal, visually blocking (and deactivating) the underlying content has appeared (e.g. registration / login modals, cookie notifications, or product information modals).
    • Inversely, when the pages underlying content becomes visible / active again, after a modal was closed.
    • 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).
  • 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).
  • A browser window / tab displaying a page is activated (in focus) after being inactive (blurred).

Installation

Collector needs to be installed as a dependency via the Yarn or npm package managers. The npm CLI ships with Node. You can read how to install the Yarn CLI in their documentation.

Depending on your preference, run one of the following.

# With Yarn
$ yarn add @sumup/collector

# With npm
$ npm install @sumup/collector

Collector requires react and react-dom v16.8+ as peer dependencies.

Usage

TrackingRoot

The TrackingRoot is responsible for storing the app value and the dispatch function. It is recommended to have only one TrackingRoot per application.

import React from 'react';
import { TrackingRoot } from '@sumup/collector';

function App() {
 const handleDispatch = React.useCallback((event) => {
   // You can define multipler handlers and transform the base event to support different schemas.
   window.dataLayer.push(event)
 }, []);

 return (
   <TrackingRoot name="app" onDispatch={handleDispatch}>
     ...
   <TrackingRoot>
 );
}

To avoid unnecessary renders, we recommend providing onDispatch as a memoized function.

The above code snippet demonstrates how to push events to the Google Tag Manager dataLayer. 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.

TrackingView

The TrackingView is responsible for storing the view value. It is recommended to have one TrackingView per "page view".

import React from 'react';
import { TrackingView } from '@sumup/collector';

function App() {
 return (
   ...
   <TrackingView name="account">
     ...
   <TrackingView>
 );
}

TrackingElement

The 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.

import React from 'react';
import { TrackingElement } from '@sumup/collector';

function App() {
 return (
   ...
   <TrackingElement name="change-account-form">
     ...
     <TrackingElement name="forgot-password">
      ...
     </TrackingElement>
   <TrackingElement>
 );
}

useClickTrigger

useClickTrigger provides you a dispatch function for any kind of click event.

The dispatch function accepts the following interface:

interface Options {
  component?: string;
  label?: string;
  customParameters?: {
    [key: string]: any,
  };
  event: 'click'; // Added internally by the hook
  timestamp: number; // Added internally when the dispatch function is called
}
import React from 'react';
import { useClickTrigger } from '@sumup/collector';

function Button({ onClick, 'tracking-label': label, children }) {
  const dispatch = useClickTrigger();
  let handler = onClick;

  if (label) {
    handler = (e) => {
      dispatch({ label, component: 'button' });
      onClick && onClick(e);
    };
  }

  return <button onClick={handler}>{children}</button>;
}

useSectionExpandedTrigger

useSectionExpandedTrigger provides you a dispatch function for a section expanded event.

The dispatch function accepts the following interface:

interface Options {
  component?: string;
  label?: string;
  customParameters?: {
    [key: string]: any,
  };
  event: 'section-expanded'; // Added internally by the hook
  timestamp: number; // Added internally when the dispatch function is called
}
import React from 'react';
import { useSectionExpandedTrigger } from '@sumup/collector';

function Section({ onClick, 'tracking-label': label, children }) {
  const dispatch = useSectionExpandedTrigger();
  let expandHandler = onClick;

  if (label) {
    expandHandler = (e) => {
      dispatch({ label, component: 'section' });
      onClick && onClick(e);
    };
  }

  return <div onClick={expandHandler}>{children}</div>;
}

useSubmitTrigger

useSubmitTrigger provides you a dispatch function for any kind of form submission event.

The dispatch function accepts the following interface:

interface Options {
  component?: string;
  label?: string;
  customParameters?: {
    [key: string]: any,
  };
  event: 'submit'; // Added internally by the hook
  timestamp: number; // Added internally when the dispatch function is called
}
import React from 'react';
import { useSubmitTrigger } from '@sumup/collector';

function Form({ children }) {
  const dispatch = useSubmitTrigger();

  const submitHandler = (e) => {
    e.preventDefault();

    dispatch({ component: 'form' });
  };

  return <form onSubmit={handler}>{children}</form>;
}

usePageViewTrigger

usePageViewTrigger() lets you dispatch a page view event.

The pageView event will be dispatched with:

interface Event {
  app: string;
  view: string;
  customParameters?: {
    [key: string]: any
  };
  event: 'page-view'; // Added internally by the hook
  timestamp: number; // Added internally by the library when the dispatch function is called
}

In order to have a meaningful page view event, we recommend integrating the available hooks for page view after declaring the TrackingRoot in your application.

You don't need to declare it after the TrackingView since any TrackingView component will overwrite the context value.

import React from 'react';
import {
  TrackingRoot,
  TrackingView,
  usePageViewTrigger,
} from '@sumup/collector';

interface Props {
  children: React.ReactNode;
  location: string;
}

// This could be a hook instead
function PageView({ location, children }: Props) {
  const dispatchPageView = usePageViewTrigger();

  // run the effect everytime location changes
  useEffect(() => {
    dispatchPageView();
  }, [location]);

  return children;
}

usePageActiveTrigger automatically dispatches an event whenever the tab becomes active again after being inactive (via Visibility change). This is meant to be used whenever you want to track if people are changing tabs.

Keep in mind only one "pageActive" trigger is required since it's a document event listener.

import React from 'react';
import { usePageActiveTrigger } from '@sumup/collector';

interface Props {
  children: React.ReactNode;
  location: string;
}

function PageActive({ location, children }: Props) {
  usePageActiveTrigger();

  return children;
}

Plugin

Helpers for specific issue.

getFlushedPayLoad

If 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.

You can find an example code here.

import React from 'react';
import { getFlushedPayload } from '@sumup/collector';

function App() {
 const handleDispatch = React.useCallback((event) => {
   // getFlushedPayload return a new event with flushed payload
   const flushedEvent = getFlushedPayload(window.dataLayer, event);
   window.dataLayer.push(flushedEvent)
 }, []);

 return (
   <TrackingRoot name="app" onDispatch={handleDispatch}>
     ...
   <TrackingRoot>
 );
}

Code of Conduct (CoC)

We 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.

If 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.

Maintainers

About SumUp

SumUp logo

SumUp 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.

Want to come work with us? Head to our careers page to find out more.