Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@sumup/collector

Package Overview
Dependencies
Maintainers
19
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sumup/collector

[![License](https://img.shields.io/badge/license-Apache%202-lightgrey.svg)](LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)

  • 1.0.0-alpha.1
  • npm
  • Socket score

Version published
Weekly downloads
1.2K
decreased by-47.97%
Maintainers
19
Weekly downloads
 
Created
Source

License Contributor Covenant

Collector

Collector is a collection of React components that facilitates user-interaction tracking for complex interfaces with a predictable event structure.

Table of contents

TLDR

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

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

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

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

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

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

Motivation

The larger web applications grows, the harder it is to provide predictable and traceable tracking structures. Consider our usual analytics event dispatching:

import React, { useContext } from 'react';
import { TrackingContext } from 'your-tracking';

function Button({ onClick, label, category, value, dispatch, children }) {
  let handler = onClick;

  if (dispatch) {
    handler = e => {
      dispatch({ label, category, value });
      onClick && onClick(e);
    };
  }

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

function AccountBalance() {
  return (
    <Button
      type="submit"
      label="show-value"
      category="balance"
      dispatch={dispatch}
    >
      Click me
    </Button>
  );
}

function AccountPage() {
  return (
    ...,
    <Balance />
  );
}

function Balance() {
  return (
   ...,
   <ShowBalance />
  );
}

function App() {
  return (
    <TrackingContext.Provider
      value={{
        dispatch: e => {
          window.dataLayer.push(e);
        }
      }}
    >
      <AccountPage />
    </TrackingContext.Provider>
  );
}

Now, what happens if we need to define the event category somewhere else in the tree? For this small example might not sound like much:

function AccountBalance({ category }) {
  const { dispatch } = useContext(TrackingContext);

  return (
    <Button
      type="submit"
      label="show-value"
      category={category}
      dispatch={dispatch}
    >
      Click me
    </Button>
  );
}

But over time this can tightly couple our component implementation with the analytics usage. Ideally the AccountBalance component shouldn't have to worry about this sort of domain.

What about leveraging our already existing TrackingContext?

function AccountBalance() {
  const { dispatch, category } = useContext(TrackingContext);

  return (
    <Button
      type="submit"
      label="show-value"
      category={category}
      dispatch={dispatch}
    >
      Click me
    </Button>
  );
}

But having a context usage also implies that you eventually need to set the value somewhere in the tree:

function AccountBalance() {
  const { dispatch, category } = useContext(TrackingContext);

  return (
    <Button
      type="submit"
      label="show-value"
      category={category}
      dispatch={dispatch}
    >
      Click me
    </Button>
  );
}

function Balance() {
  const { dispatch, setValue } = useContext(TrackingContext);
  useEffect(() => {
    setValue({ category: 'account-balance' });
  }, []);

  return (
   ...,
   <ShowBalance />
  );
}

function App() {
  const [trackingData, setTrackingData] = useState({});
  return (
    <TrackingContext.Provider
      value={{
        ...trackingData,
        dispatch: e => {
          window.dataLayer.push(e);
        },
        setValue: value => setTrackingData({ ...trackingData, ...value })
      }}
    >
      <AccountPage />
    </TrackingContext.Provider>
  );
}

But again, over time this can tightly couple our component implementation with the analytics usage, and the more fields you need to overwrite, the harder it is to reason about the current state of the context, and that's where Collector can help you!

Collector was built to track user-interactions with high granularity. Using an agnostic event schema you can serve different tracking purposes with it. Consider the same example using Collector:

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

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

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

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

function AccountBalance() {
  return (
    <Button type="submit" tracking-label="show-balance">
      Click me
    </Button>
  );
}

function AccountPage() {
  return (
    <TrackingView name="account">
      ...,
      <Balance />
    </TrackingView>
  );
}

function Balance() {
  return (
    <TrackingZone name="balance">
      ...,
      <ShowBalance />
    </TrackingZone>
  );
}

function toAnalyticsEvent({ view, zone, label, action }) {
  return {
    category: `${view}-${zone}`,
    label: label,
    action
  };
}

function App() {
  return (
    <TrackingRoot
      name="my-app"
      onDispatch={event => {
        window.dataLayer.push(toAnalyticsEvent(event));
      }}
    >
      <AccountPage />
    </TrackingRoot>
  );
}

For more information about the event schema and component structure, please refer to the Usage section.

Installing

NPM

npm install @sumup/collector

yarn

yarn add @sumup/collector

Usage

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/zone 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
  zone?: string; // The current "feature"/"organism", such as a login form. Can be overwritten
  component?: 'button' | 'link'; // Which primitive dispatched the event
  label?: string;
  event: 'click' | 'view' | 'load' | 'page-view' | 'submit' | 'browser-back'; // This action is handled internally based on the kind of event you dispatched.
  customParameters?: {
    [key: string]: any;
  };
  timestamp: number; // Provided by the library when the dispatch function is called
}

The directives (Root = app, View = view and Zone = zone) 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">
    ...
    <TrackingZone name="change-account-form">
     ...
    </TrackingZone>
   </TrackingView>
 <TrackingRoot>

Would yield the following structure: { app: 'my-app', view: 'account', zone: 'change-account-form' }. You can also overwrite Zone for complex trees:

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

Would yield the following structure: { app: 'my-app', view: 'account', zone: 'validate-account-digit' }. While it may not sound like much, it is really useful for larger applications.

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.

TrackingView

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

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

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

TrackingZone

The TrackingZone is responsible for storing the zone value. Zones are usually a representation of a feature/organism in your application such as a form.

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

function App() {
 return (
   ...
   <TrackingZone name="change-account-form">
     ...
   <TrackingZone>
 );
}

Dispatching events

Here are a list of supported events you can dispatch using pre-defined hooks:

  • click
  • pageView
  • view (to be implemented)
  • load (to be implemented)
  • submit (to be implemented)
  • browserBack (to be implemented)

Click

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
  };
}
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>;
}

PageView

What can be considered a page view?

  • Page load
  • Route changes in SPAs
  • New "context" over the screen being displayed, such as modals.

The pageView event will be dispatched with:

interface Event {
  app: string;
  view: string;
  event: 'page-view'; // Provided by available hooks
  timestamp: number; // Provided by the library when the dispatch function is called
}

Where to place the page view hook in your application

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.

Available hooks

usePageViewTrigger() lets you dispatch a page view event.

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 dispatchs an event whenever the tab becomes inactive and then active again (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;
}

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.

FAQs

Package last updated on 24 Jun 2020

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc