@volvo-cars/tracking
TypeScript icon, indicating that this package has built-in type declarations

2.7.0 • Public • Published

Tracking

Questions? Ask in Slack #vcc-ui

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

PageTrackingProvider

To send page load events without needing user input, immediately on page load, you can be done by adding the PageTrackingProvider. There can only be one PageTrackingProvider in the tree per page.

render(
  <PageTrackingProvider pageName="Landing Page" pageType="Campaign">
    {children}
  </PageTrackingProvider>
  {/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
  <DataLayerViewer />
);

Notice how the events pushed to window.dataLayer in the above example include pageName and pageType.

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.

render(
  <PageTrackingProvider pageName="Landing Page" pageType="Campaign" forceLowerCase={false}>
    {children}
  </TrackingProvider>
  {/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
  <DataLayerViewer />
);

logging

We can enable logging of events in development with the logging prop on the PageTrackingProvider.

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 TrackingProviders, 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 [DEPRECATED] use PageTrackingProvider

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.

Track

Allows any child element to attach tracking events to any event.

const Wrapper = ({ children }) => {
  return (
    <TrackingProvider pageName="landing page" enableReactTracking={false}>
      <TrackingProvider
        eventCategory="promotional hero"
        enableReactTracking={false}
      >
        {children}
      </TrackingProvider>
      {/* ↓↓↓ this is only to view the `window.dataLayer` in the preview above.*/}
      <DataLayerViewer />
    </TrackingProvider>
  );
};

const Component = () => {
  return (
    <View>
      <Track eventLabel="cta11">
        <Button>Button 1</Button>
      </Track>
      <Spacer />
      <Track eventLabel="cta22">
        <Button>Button 2</Button>
      </Track>
    </View>
  );
};
render(
  <Wrapper>
    <Component />
  </Wrapper>
);

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:

  1. Direct child is a simple dom element.
  2. 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
options.forceLowerCase Force all event values to be lowercase. boolean true
options.deferNonInteraction Defer nonInteraction events until a pageType or pageName event is present in the dataLayer. boolean true
options.logging Log all sent events to the console. boolean false
options.disabled Disables sending events. boolean false
options.mode Decides what events to send for ga3 or ga4 or both string ga3
options.ga3 GA3 values to keep sending to ga3 GA3Event undefined

Tracker.interaction(eventData?: TrackingData)

Pushes an event: interaction event to the data layer.

Tracker.nonInteraction(eventData?: TrackingData)

Pushes an event: noninteraction event to the data layer. If no object with the pageType or pageName is in the data layer yet, nonInteraction events are queued for up to 90 seconds to avoid affecting the order of events used to determine bounce rates.

Tracker.virtualPageView(name: string, value?: string)

Pushes an event: virtualPageView event to the data layer.

Tracker.pushCustomDimension(name: string, value?: string)

Pushes a custom dimension to the data layer.

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
trackerOptions Any trackerOptions to be forwarded to the Tracker TrackerOptions
options?.ignoreifEmptyContext Disables the Tracker if no top level TrackerProvider wraps the tree boolean false
options?.mode Decides what events to send for ga3 or ga4 or both string ga3
options.ga3 GA3 values to keep sending to ga3 GA3Event undefined

Props - TrackingProvider

Name Description Type Default Value
enableReactTracking If disabled, data- attributes are used to maintain tracking data instead of React context. boolean true
deferNonInteraction Defer nonInteraction events until a pageType or pageName event is present in the dataLayer. boolean true
trackPageLoad [DEPRECATED] Automatically sends pageLoad event. boolean false
forceLowerCase Force all event values to be lowercase. boolean false
logging Enable logging of sent analytics data in development. boolean false
mode Decides what events to send for ga3 or ga4 or both string ga3
event What type of event to pass down to children. Only works for ga4 string custom_event
ga3 GA3 values to keep sending to ga3 GA3Event undefined
...rest Any other props will be sent as tracking data Object

Props - PageTrackingProvider

Name Description Type Default Value
enableReactTracking If disabled, data- attributes are used to maintain tracking data instead of React context. boolean true
forceLowerCase Force all event values to be lowercase. boolean false
logging Enable logging of sent analytics data in development. boolean false
mode Decides what events to send for ga3 or ga4 or both string ga3
ga3 GA3 values to keep sending to ga3 GA3Event undefined
...rest Any other props will be sent as tracking data Object

Props - Track

Name Description Type Default Value
children A single React node, Fragments not supported React.node
eventAction Optional action to send with analytics data. string click
domEvent A dom/react event to watch and attach tracking data to string onClick
eventLabel Label to send with analytics data. string
customData Custom tracking data to pass through to Tracker TrackingData
event What type of event to send. Only works for ga4 string custom_event
ga3 GA3 values to keep sending to ga3 GA3Event undefined

withTracker

withTracker(
  Component: React.ComponentType,
  {
    event: 'onClick';
    defaultAction: string;
    ga3?: GA3Event;
  });

returns a new React.ComponentType with tracking data attached.

Name Description Type Default Value
Component Any valid React component React.ComponentType
event onClick event string
defaultAction Default action to be sent with the event string
trackEvent What type of event to send. Only works for ga4 string custom_event
trackGA3 GA3 values to keep sending to ga3 GA3Event undefined

createDomTrackingListener

createDomTrackingListener(eventName:string, options?: TrackerOptions, event?: TrackEvent )

return a new event listener.

Google Analyctis 4

Tracker.sendEvent(event?: TrackEvent, eventData?: TrackingData)

Pushes an event: TrackEvent event to the data layer.

TrackEvent =
  | 'chat_interaction'
  | 'custom_event'
  | 'sitenav_interaction'
  | 'view_item'
  | 'page_view'
  | 'web_vitals'
  | 'add_to_cart'
  | 'cart_view'
  | 'begin_checkout'
  | 'add_payment_info'
  | 'purchase'
  | 'config_start'
  | 'config_finish'
  | 'login'
  | 'account_created'
  | 'form_load'
  | 'form_submit'
  | 'search';

Tracker.customEvent(customEventData: CustomEventData)

Pushes an event: custom_event event to the data layer.

Tracker.siteNavInteraction(siteNavInteractionData: SiteNavInteractionData)

Pushes an event: site_navInteraction event to the data layer.

Tracker.chatInteraction(chatInteractionData: ChatInteractionData)

Pushes an event: chat_interaction event to the data layer.

Tracker.pageView(pageViewData: PageViewData)

Pushes an event: page_view event to the data layer.

Tracker.viewItem(viewItemData: ViewItemData)

Pushes an event: view_item event to the data layer.

Tracker.addToCart(addToCartData: AddtoCartData)

Pushes an event: add_to_cart event to the data layer.

Tracker.cartView(cartViewData: CartViewData)

Pushes an event: cart_view event to the data layer.

Tracker.beginCheckout(beginCheckoutData: BeginCheckoutData)

Pushes an event: begin_checkout event to the data layer.

Tracker.addPaymentInfo(addPaymentInfoData: AaddPaymentInfoData)

Pushes an event: add_payment_info event to the data layer.

Tracker.purchase(purchaseData: PurchaseData)

Pushes an event: purchase event to the data layer.

Tracker.configStart(configStartData: ConfigStartData)

Pushes an event: config_start event to the data layer.

Tracker.configFinish(configFinishData: ConfigFinishData)

Pushes an event: config_finish event to the data layer.

Tracker.formLoad(formLoadData: FormLoadData)

Pushes an event: form_load event to the data layer.

Tracker.formSubmit(formSubmitData: FormSubmitData)

Pushes an event: form_submit event to the data layer.

Tracker.webVitals(webVitalsData: WebVitalsData)

Pushes an event: web_vitals event to the data layer.

How to upgrade to Google Analytics 4

Start to set mode to both and set logging to true to see that two events are sent in the console.

<TrackingProvider logging mode="both">
  children
</TrackingProvider>

To have every event sent for example add_to_cart

<TrackingProvider event="add_to_cart" logging mode="both">
  children
</TrackingProvider>

You can override event in children to set a new event prop for example

<TrackingProvider event="add_to_cart" logging mode="both">
  children
  <TrackingProvider event="purchase" logging mode="both">
    children
  </TrackingProvider>
</TrackingProvider>

You can also specify on every child for example

<TrackingProvider event="add_to_cart" logging mode="both">
  children
  <Click trackEvent="purchase" trackEventLabel="label">
    button
  </Click>
  <Track event="add_payment_info" eventLabel="label">
    <>
      <button />
      <button />
    </>
  </Track>
</TrackingProvider>

You can now also use our new typed functions instead

const tracker = new Tracker(null, { mode: 'both' });
tracker.addToCart({
  countryCode: 'countryCode',
  pageName: 'pageName',
  pageType: 'pageType',
  ecommerce: {
    items: [
      {
        itemName: 'itemName',
      },
    ],
  },
});

Readme

Keywords

none

Package Sidebar

Install

npm i @volvo-cars/tracking

Weekly Downloads

2,247

Version

2.7.0

License

UNLICENSED

Unpacked Size

197 kB

Total Files

31

Last publish

Collaborators

  • sylvainestevezvolvocars
  • allenbargi-vcc
  • jacobrask
  • glenashley
  • volvocars-uip-bot
  • alizeait
  • kristiankalb
  • samny_volvocars