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:
In order to support the app/view/elements hierarchy, the event schema is defined by the following keys:
interface Event {
app: string;
view: string;
elementTree: string[];
component?: 'button' | 'link';
label?: string;
event:
| 'click'
| 'view'
| 'load'
| 'page-view'
| 'page-reactivated'
| 'submit'
| 'browser-back';
timestamp: number;
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.
$ yarn add @sumup/collector
$ 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) => {
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 the events to the Google Analytics data layer. This is just an example, Collector is agnostic of the 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';
timestamp: number;
}
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>;
}
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';
timestamp: number;
}
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;
event: 'page-view';
timestamp: number;
}
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;
}
function PageView({ location, children }: Props) {
const dispatchPageView = usePageViewTrigger();
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) => {
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 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.