README
Tracking
@volvo-cars/tracking
A declarative way to add Google Tag Manager tracking data to your application. It supports multiple ways of sending analytics events by supporting event data inheritance and the possibility to send events with vanilla JavaScript without rendering/hydrating your React application.
Installation
💡 This package includes Typescript definitions
useTracker
The simplest way of adding an event to GTM is by using the useTracker
hook which returns a Tracker
instance that exposes helpful methods that send different event types such as interaction
or noninteraction
. It sends these events by modifying the window.dataLayer
global array. This array is watched for changes by GTM when embedded on a page, which means any pushes to this array will trigger a new GTM event.
() => {
const tracker = useTracker();
return (
<View>
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta1',
});
}}
>
Button 1
</Button>
<Spacer />
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta2',
});
}}
>
Button 2
</Button>
{/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
<DataLayerViewer />
</View>
);
};
In the above example, we send an interaction event and attach eventAction
and eventLabel
to it. Notice the event additions to window.dataLayer
on each button click. It's also worth noticing how Tracker.interaction
adds the event
property automatically to each event. This is to distinguish between the types of events sent to GTM.
Arguments
useTracker
takes 3 optional arguments. The first is an event data object that will be added to all events sent by the returned Tracker
. The second is any Tracker
options and the third is options?.ignoreIfEmptyContext
which is used in conjunction with the TrackingProvider
mentioned below, this will disable sending events if the hook is not wrapped with a parent TrackingProvider
.
TrackingProvider
Simple
While useTracker
works fine for simple cases, we sometimes want to send shared data between all events without needing to rewrite said data with every event we send. This can be done by passing the default data as props to TrackingProvider
.
const Component = () => {
const tracker = useTracker();
return (
<View>
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta1',
});
}}
>
Button 1
</Button>
<Spacer />
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta2',
});
}}
>
Button 2
</Button>
</View>
);
};
const Wrapper = ({ children }) => {
return (
<>
<TrackingProvider pageName="landing page" eventCategory="category">
{children}
</TrackingProvider>
{/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
<DataLayerViewer />
</>
);
};
render(
<Wrapper>
<Component />
</Wrapper>
);
Notice how the events pushed to window.dataLayer
in the above example include pageName
and eventCategory
.
Inheritance
TrackingProvider
supports inheritance, meaning that for any data added to any of the parant TrackingProvider
s, all will be sent and not just the last in the tree.
const Component = () => {
const tracker = useTracker();
return (
<View>
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta1',
});
}}
>
Button 1
</Button>
<Spacer />
<Button
onClick={() => {
tracker.interaction({
eventAction: 'click',
eventLabel: 'cta2',
});
}}
>
Button 2
</Button>
</View>
);
};
const Wrapper = ({ children }) => {
return (
<>
<TrackingProvider pageName="landing page">
<TrackingProvider pageName="landing page" eventCategory="category">
<TrackingProvider
customEventData="custom data"
eventCategory="override category"
>
{children}
</TrackingProvider>
</TrackingProvider>
</TrackingProvider>
{/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
<DataLayerViewer />
</>
);
};
render(
<Wrapper>
<Component />
</Wrapper>
);
In the above example, all event data from all TrackingProviders was sent with each event in that tree. Notice how eventCategory
was overridden in the last TrackingProvider.
trackPageLoad
It's sometimes desired to send page load events without needing user input. This can be done by adding the trackPageLoad
prop on any TrackingProvider
in the tree.
() => {
return (
<TrackingProvider trackPageLoad pageType="pdp" pageName="xc40">
<Block>...</Block>
</TrackingProvider>
);
};
forceLowerCase
All events are forced to be lowercase by default but it's also possible to disable this behaviour, based on specific requirements. This can be done with the forceLowerCase
prop.
const Component = () => {
const tracker = useTracker();
return (
<View>
<Button
onClick={() => {
tracker.interaction({
eventAction: 'Click',
eventLabel: 'Cta1',
});
}}
>
Button 1
</Button>
<Spacer />
<Button
onClick={() => {
tracker.interaction({
eventAction: 'Click',
eventLabel: 'CTA2',
});
}}
>
Button 2
</Button>
</View>
);
};
const Wrapper = ({ children }) => {
return (
<>
<TrackingProvider pageName="landing PAGE" forceLowerCase={false}>
<TrackingProvider customEventData="CUStom data">
{children}
</TrackingProvider>
</TrackingProvider>
{/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
<DataLayerViewer />
</>
);
};
render(
<Wrapper>
<Component />
</Wrapper>
);
logging
We can enable logging of events in development with the logging
prop on the TrackingProvider
.
enableReactTracking
TrackingProvider
can store information in data
attributes. This allows us to push event tracking information
in places where we don't want to hydrate/render our React application. A useful case is for static sites that don't need any user input except for sending tracking events. A useful usecase is the DotCom SiteFooter which is rendered using React server-side but does not render/hydrate client-side.
This can be done by disabling the enableReactTracking
prop on the TrackingProvider
. A more detailed explanation can be found in the Dom Tracking.
withTracker
This package also exports a HOC that helps with attaching tracking events based on domEvents
const Wrapper = ({ children }) => {
return (
<TrackingProvider pageName="landing page">
<TrackingProvider eventCategory="promotional hero">
{children}
</TrackingProvider>
{/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
<DataLayerViewer />
</TrackingProvider>
);
};
const TrackedButton = withTracker(Button, {
event: 'onClick',
defaultAction: 'click',
});
const Component = () => {
return (
<View>
<TrackedButton trackEventLabel="cta1">Button 1</TrackedButton>
<Spacer />
<TrackedButton trackEventLabel="cta2">Button 2</TrackedButton>
</View>
);
};
render(
<Wrapper>
<Component />
</Wrapper>
);
Dom Tracking
As mentioned earlier in the enableReactTracking section. TrackingProvider
can store information in data
attributes. This allows us to push event tracking information in places where we don't want to hydrate/render our React application.
To enable this, first set enableReactTracking
to false on the TrackingProvider
.
This will generate the following html, notice the data-track-onclick
attributes.
const Wrapper = ({ children }) => {
return (
<div id="root">
<FelaWrapper>
<TrackingProvider pageName="landing page" enableReactTracking={false}>
<TrackingProvider
eventCategory="promotional hero"
enableReactTracking={false}
>
{children}
</TrackingProvider>
</TrackingProvider>
</FelaWrapper>
</div>
);
};
const TrackedButton = withTracker(Button, {
event: 'onClick',
defaultAction: 'click',
});
const Component = () => {
return (
<View>
<TrackedButton trackEventLabel="cta1">Button 1</TrackedButton>
<Spacer />
<TrackedButton trackEventLabel="cta2">Button 2</TrackedButton>
</View>
);
};
render(() => {
const decodeHtml = function decodeHtml(html) {
if (typeof window === 'undefined') return '';
var txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
};
return (
<>
<code>
{decodeHtml(
ReactDomRenderToStaticMarkup(
<Wrapper>
<Component />
</Wrapper>
)
)}
</code>
</>
);
});
We then attach the tracking listeners with createDomTrackingListener
.
import { createDomTrackingListener } from '@volvo-cars/tracking/domTracking';
document
// first get the parent of the nested components which have tracking
.getElementById('root')
// attach the listener which will traverse up the tree to get all the
// context data from parent `data` attributes
?.addEventListener('click', createDomTrackingListener('onClick'));
Caveats
When rendering the static content, and wanting to use the TrackingProvider
passing custom react components will just pass in props with data
. It's up
to you to pass them down the line. It will only add them in two cases:
- Direct child is a simple dom element.
- Children is a fragment or multiple elements, in this case, the'll be wrappedj with div
Examples:
// this will add data attributes
// to the `main`
<TrackingProvider
pageType="catch all"
enableReactTracking={false}
>
<main>
{/* .... */}
</main>
</TrackingProvider>
// this will wrap children
// with `div` containing data
<TrackingProvider
pageType="catch all"
enableReactTracking={false}
>
{header}
<main>
{/* .... */}
</main>
</TrackingProvider>
// this will pass down `data-track-context`
// to the `App` it's up to developer to handle this
<TrackingProvider
pageType="catch all"
enableReactTracking={false}
>
<App />
</TrackingProvider>
Strict types
This package exports non-strict types for TrackingData
and CustomDimension
export interface TrackingData extends Record<string, any> {}
export type CustomDimension = string;
Those types can be made stricter depending on your use case. To override those types you can create a declartions file in your types directory somewhere in your application and overide them as needed.
Example
import '@volvo-cars/tracking';
declare module '@volvo-cars/tracking' {
export interface TrackingData {
eventAction?: string;
eventLabel?: string;
eventCategory?: string;
}
}
Web Vitals
You can measure Web Vitals metrics on real users, in a way that accurately matches how they're measured by Chrome and reported to other Google tools. This can be done in two ways depending on the use case:
Using Next.js
Starting from Next.js v10.0.0, you can export a reportWebVitals
function from _app
which helps provide Web Vital metrics:
import { reportWebVitals as webVitals } from '@volvo-cars/tracking/webVitals';
export function reportWebVitals(metrics: NextWebVitalsMetric) {
return webVitals({
metrics,
});
}
This will report something like the following, depending on the metric dispatched by Next.js
{
event: 'noninteraction',
eventAction: 'fcp',
eventCategory: 'web vitals',
eventLabel: 'uniqueid',
eventValue: 2,
},
Any additional event data can be sent using additionalEventData
property:
import { reportWebVitals as webVitals } from '@volvo-cars/tracking/webVitals';
export function reportWebVitals(metrics: NextWebVitalsMetric) {
return webVitals({
metrics,
additionalEventData: {
pageName: 'my page name',
pageType: 'my page type',
},
});
}
Custom App
If not using Next.js, Web Vitals can be reported and measured using measureWebVitals
:
import { measureWebVitals } from '@volvo-cars/tracking/webVitals';
measureWebVitals();
API
Tracker
constructor(
eventData?: TrackingData | null,
{
forceLowerCase = true,
logging = false,
disabled = false,
}: TrackerOptions = {}
)
Name | Description | Type | Default Value |
---|---|---|---|
eventData |
Default event data to be sent with every event | Object |
undefined |
trackerOptions.forceLowerCase |
Force all event values to be lowercase | boolean |
true |
trackerOptions.disabled |
Disables sending events | boolean |
false |
Tracker.interaction(eventData?: TrackingData) |
Sends an event with event as interaction |
Function |
Function |
Tracker.nonInteraction(eventData?: TrackingData) |
Sends an event with event as noninteraction |
Function |
Function |
Tracker.virtualPageView(eventData?: TrackingData) |
Sends an event with event as virtualPageView |
Function |
Function |
Tracker.pushCustomDimension(name: CustomDimension, value?: string) |
Pushes a custom event | Function |
Function |
useTracker
useTracker(
hookData?: TrackingData | null,
trackerOptions?: TrackerOptions,
options?: { ignoreIfEmptyContext?: boolean }
): Tracker
returns a Tracker
instance.
Name | Description | Type | Default Value |
---|---|---|---|
hookData |
Any default Tracking data to be added to Tracker eventData | Object, null |
undefined |
trackerOptions |
Any trackerOptions to be forwarded to the Tracker | TrackerOptions |
undefined |
options?.ignoreifEmtpyContext |
Disables the Tracker if no top level TrackerProvider wraps the tree | boolean |
undefined |
Props - TrackingProvider
Name | Description | Type | Default Value |
---|---|---|---|
trackPageLoad |
Automatically sends pageLoad event | boolean |
undefined |
forceLowerCase |
Force all event values to be lowercase | boolean |
false |
logging |
Enable logging of sent analytics data in development | boolean |
false |
enableReactTracking |
If disabled, data- attributes are used to maintain tracking data |
boolean |
true |
...rest |
Any other props will be sent as tracking data | Object |
undefined |
withTracker
withTracker(
Component: React.ComponentType,
{
event: 'onClick';
defaultAction: string;
});
returns a new React.ComponentType
with tracking data attached.
Name | Description | Type | Default Value |
---|---|---|---|
Component |
Any valid React component | React.ComponentType |
n/a |
event |
onClick event |
string |
n/a |
defaultAction |
Default action to be sent with the event | string |
n/a |
createDomTrackingListener
createDomTrackingListener(eventName:string)
return a new event listener.