
Research
Security News
Lazarus Strikes npm Again with New Wave of Malicious Packages
The Socket Research Team has discovered six new malicious npm packages linked to North Korea’s Lazarus Group, designed to steal credentials and deploy backdoors.
@sumup/collector
Advanced tools
[](LICENSE) [](CODE_OF_CONDUCT.md)
Collector is a collection of React components that facilitates user-interaction tracking for complex interfaces with a predictable event structure.
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>
);
}
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.
npm install @sumup/collector
yarn add @sumup/collector
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/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.
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 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>
);
}
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>
);
}
Here are a list of supported events you can dispatch using pre-defined hooks:
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>;
}
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
}
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.
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;
}
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.
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
Unknown package
We found that @sumup/collector demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 17 open source maintainers collaborating on the project.
Did you know?
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.
Research
Security News
The Socket Research Team has discovered six new malicious npm packages linked to North Korea’s Lazarus Group, designed to steal credentials and deploy backdoors.
Security News
Socket CEO Feross Aboukhadijeh discusses the open web, open source security, and how Socket tackles software supply chain attacks on The Pair Program podcast.
Security News
Opengrep continues building momentum with the alpha release of its Playground tool, demonstrating the project's rapid evolution just two months after its initial launch.