Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
  dist
  package.json
  README.md
Size: Mime:
  README.md

@doodle/tracking

Table of contents

  1. Overview
    1. Tracking Intent
    2. Cookie and Consent Management
  2. Initial setup
    1. Additional setup
  3. Declarative API
  4. Imperative API
  5. Consent Management API
  6. Real world examples
  7. Mapping to destination services
    1. Amplitude
    2. Google Analytics
  8. Development of this package
  9. Publishing of this package
    1. Publish release candidate
    2. Publish release

1. Overview

@doodle/tracking provides declarative and imperative APIs for tracking, both centered around the same interface: a Tracking Intent, which allows handling multiple tracking destination services, with mappers to each expected format. For now there is support for:

To track declaratively, pass a tracking intent to getTrackingDataAttrs and spread the returned value over your clickable element. A global click listener will pick up resulting data attributes and dispatch accordingly.

When a declarative approach is not possible, invoke analytics.track with a tracking intent directly. For example when tracking from a Redux Saga or after a specific widget behavior.

1.1. Tracking Intent

Each tracking intent contains 1 or more of the following top-level keys:

  • track: for events or actions the user performs, e.g. clicks in buttons, links, widgets
  • identify: for user identifications, usually once or twice per page load, e.g. passing an anonymous token on first hit of entry points pages (Login/Signup/Static Site) and then, after authorization, another call to inform of the authenticated accesses. Eventually, there may be other calls, say on experiments, or if a user updates their profile.
  • page: for page views, when there are full page reloads (window.location assignments) or SPA route changes (aka "virtual page views")
  • options: to configure tracking behavior per tracker, e.g. whether an item should be automatically tracked on every click, or to which destination services.

Below is the shape for each top-level field. Mandatory ones are in bold:

track (Object)

Property Type Description
event string The name of the event you are tracking, following Doodle Data Layer Taxonomy, e.g. 'Click Compare Plans', 'Click FAQs' or 'Start Trial'
properties Object Custom event properties
properties.category string A label for the track event, usually a page name like 'Checkout' or 'Pricing Page'
properties.description string A summary of the track event
type string Default: 'user Interaction'. Another valid value: 'conversion'

identify (Object)

Property Type Description
userId string Id of the user in Doodle
properties Object A mapping of properties you know about the user. Things like email, name or friends

page (Object)

Property Type Description
name string The name of the page
properties Object Custom event properties
properties.path string Path portion of the URL of the page. Equivalent to canonical path which defaults to location.pathname from the DOM API
properties.title string Title of the page. Equivalent to document.title from the DOM API
properties.url string Full URL of the page. First we look for the canonical url. If the canonical url is not provided, we use location.href from the DOM API
properties.referrer string Full URL of the previous page. Equivalent to document.referrer from the DOM API
properties.category string The category of the page. Useful for cases where many pages might live under a single category.

Finally, a tracking intent may contain an options top-level key:

options (Object)

Property Type Description
autoTracking boolean Default: true. Controls automated declarative tracking behavior for a specific tracked item
services Object Controls disabling specific services
services.amplitude boolean Default: true. Dispatches tracking data to Amplitude
services.doodleDataLayer boolean Default: true. Dispatches tracking data to Doodle's Data Layer
services.ga boolean Default: true. Dispatches tracking data to Google Analytics

1.2. Cookie and Consent Management

@doodle/tracking has built-in support for Cookie and Consent Management by leveraging OneTrust. This enables frontends using @doodle/tracking to be able to conform to GDPR/CCPA laws and guidelines.

By providing a OneTrust site ID (or sometimes referred to as Script ID) to API.init, the OneTrust Consent Management script will be automatically injected into the page and trackers will only be loaded/initialized once appropriate consent has been given.

This works by checking if the user has given consent to the consent group (eg "Performance Cookies") through the Consent Management prompt. Internally, these consent groups are identified by IDs (eg C0002, sometimes also referred to as "active groups"). Once the user gives consent to the consent category whose ID matches the ID associated with a tracker, the tracker will be loaded/initialized. For the mappings, see dispatchers.config.js and constants.js.

Trackers are also unloaded/removed automatically if the user changes their consent settings at a later point. Besides blocking all subsequent tracking calls of the tracker, cookies and localstorage items are cleaned up as part of the removal as well.

Another important feature of OneTrust is support for IAB TCF. This framework is mainly used by Ad vendors to obtain user consent (for example to allow personalised ads). To achieve this, OneTrust automatically initialises the corresponding Ad consent groups and exposes the TCF API in the browser.

2. Initial setup

  1. If you do not have it yet, add Gemfury's private registry configuration to your project:

    echo "registry=https://npm-proxy.fury.io/mfsTqYdDz3bsKFQJuMAR/tmf/" >> .npmrc
    
  2. Install with yarn add @doodle/tracking

  3. In the client side initialization of your application, insert a call to API.init. That is usually from where you run your rootSaga, e.g. in Billing that's src/client.js. You will want to make the Amplitude key secret available at runtime as an environment variable then use it like so:

    import * as Sentry from '@sentry/browser';
    import { API } from '@doodle/tracking';
    import * as Avo from '../Avo'; // needed when Avo service is enabled
    
    export const analytics = API.init({
      clientId: 'web-scheduling-experience', // or web-billing (following kubernetes service name)
      clientVersion: 'rel.v38',
      avo: Avo, // needed when Avo service is enabled
      env: {
        avoApiKey: process.env.AVO_API_KEY, // needed when Avo Inspector service is enabled
        amplitudeApiKey: process.env.AMPLITUDE_API_KEY, // needed when Amplitude service is enabled
        svcDataLayerApi: process.env.SVC_DATA_LAYER_API, // needed when Doodle Data Layer service is enabled, also used by Avo
        doodleEnv: process.env.DOODLE_ENV, // needed when Google Analytics service is enabled
        nodeEnv: process.env.NODE_ENV, // needed when Google Analytics service is enabled
        oneTrustScriptId: process.env.ONETRUST_SCRIPT_ID, // if provided, Consent Managemnt will be enabled
        cookieDomain: process.env.COOKIE_DOMAIN, // Optionally, a custom cookie domain to be used when removing cookies if service is removed
      },
      errorHandler: Sentry.captureException,
      services: { amplitude: true, doodleDataLayer: true, ga: false, avo: false, avoInspector: false }, // enable or disable specific tracking services
    });
    
  4. Add a identify call on the authentication step of your app. This can be done by taking the @doodle/user/LOAD_USER Redux action in projects that adopt @doodle/users-api-connector, or a dependent app action like in Dashboard:

    import { call, select, takeLatest } from 'redux-saga/effects';
    
    function* onUserLoaded() {
      const id = yield select(state => state.user.data.id);
    
      yield call(analytics.identify, { trackingIntent: { identify: { userId: id } } });
    }
    
    function* watchUserLoaded() {
      yield takeLatest(UserActionTypes.USER_LOADED, onUserLoaded);
    }
    

    After that all tracking calls will be embedded with that user id.

  5. If you want to identify the anon user you'll need to pass user id as a null value. It's important to pass null as a value for user id so amplitude can set a valid id for that user in their dashboard. Usually, there is no need for identifying anon users, but in some situations we might need to do so (e.g. identifying anon participant).

    import { call, takeLatest } from 'redux-saga/effects';
    
    function* onParticipate(email) {
      const emailGroup = /@doodle|@doodle-test\.com$/i.test(email) ? 'Doodle' : 'Non Doodle';
      const trackingIntent = {
        identify: { 
          userId: null, 
          properties: { 
            Email: emailGroup,
          }, 
        },
      };
    
      yield call(analytics.identify, { trackingIntent });
    }
    
    function* watchUserParticipate() {
      yield takeLatest(UserActionTypes.PARTICIPATE, onParticipate);
    }
    

    After that all tracking calls will be embedded with that user id.

NOTE: After identifying, if your app logs out without reloading the page you need to call analytics.reset() to avoid tracking with a wrong user id.

  1. Start tracking!

2.1 Additional setup

If you plan to use either Amplitude or Avo Inspector with @doodle/tracking, you must provide the client as a dependency in your application. This was done to avoid having to bundle these service SDKs with the @doodle/tracking package.

See package.json for the required version ranges.

3. Declarative API

NOTE: In case the data to be tracked is dynamic, e.g. when we track parts of the current state of the application, it is preferrable to go with the imperative API. This will avoid DOM re-renders caused by the update of the data tracking attributes. However for cases like the below, where the tracked data is static, the declarative approach may be more desirable.

For track:

import { getTrackingDataAttrs } from '@doodle/tracking';

const MyCTA = () => {
  const trackingIntent = {
    track: {
      event: 'Click Trial Button',
      type: 'user Interaction',
      properties: {
        category: 'Pricing Page',
        description: 'User clicks on "Start free Trial"',
        'Trial Premium Plan': 'Starter',
      },
    },
  };

  return (
    <Button {...getTrackingDataAttrs(trackingIntent)}>
      <span>Start free Trial</span>
    </Button>
  );
}

For identify:

import { getTrackingDataAttrs } from '@doodle/tracking';

const MyPageWrapper = () => {
  const trackingIntent = {
    identify: { userId: '123456' },
  };

  return (
    <article {...getTrackingDataAttrs(trackingIntent)}>
      ...
    </article>
  );
}

Identify anon user:

import { getTrackingDataAttrs } from '@doodle/tracking';

const MyPageWrapper = () => {
  const trackingIntent = {
    identify: { userId: null },
  };

  return (
    <article {...getTrackingDataAttrs(trackingIntent)}>
      ...
    </article>
  );
}

For page (WIP):

import { getTrackingDataAttrs } from '@doodle/tracking';

const MyPageWrapper = () => {
  const trackingIntent = {
    page: {
      name: 'Pricing Page',
      properties: {
        path: '/premium',
        title: 'Pricing Page',
        url: 'http://doodle.com',
      },
    },
  };

  return (
    <article {...getTrackingDataAttrs(trackingIntent)}>
      ...
    </article>
  );
}

4. Imperative API

At each module you need to track, first import the analytics instance you have initialized in your application startup. It has 3 methods you can use: analytics.track, analytics.page and analytics.identify.

Each accepts as first argument an object containing either a trackingIntent or a trackingEl key. If both are provided, the intent is preferred and will be used instead of the element's data attributes when mapping the tracking data for dispatch.

Below are three examples with trackingIntent and one with trackingEl:

import { analytics } from '../../analytics';

const MyCTA = () => {
  const trackingIntent = {
    track: {
      event: 'Click Trial Button',
      type: 'user Interaction',
      properties: {
        category: 'Pricing Page',
        description: 'User clicks on "Start free Trial"',
        'Trial Premium Plan': 'Starter',
      },
    },
  };

  const handleClick = trackingIntent => event => {
    analytics.track({ trackingIntent });
  }

  return (
    <Button onClick={handleClick(trackingIntent)}>
      <span>Start free Trial</span>
    </Button>
  );
}

In the second example we call analytics.identify in a class component. Note this time we use <form>'s onSubmit prop to ensure we do not track in case of failed validations.

import { analytics } from '../../analytics';

class MyCTA extends Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit() {
    const trackingIntent = {
      identify: {
        userId: 'doodleUserId',
        properties: {
          isOnExperimentWEB3000: true,
        },
      },
    };
    analytics.identify({ trackingIntent });
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <Button><span>Start free Trial<span></Button>
      </form>
    )
  }
}

The third example is with a saga worker:

import { call } from 'redux-saga/effects';
import { analytics } from '../../analytics';

// You may want to group all tracking intents in a separate module
const onMyActionTrackingIntent = {
  track: {
    event: 'Click Trial Button',
    type: 'user Interaction',
    properties: {
      category: 'Pricing Page',
      description: 'User clicks on "Start free Trial"',
      'Trial Premium Plan': 'Starter',
    },
  },
};

export function* onMyAction() {
  try {
    yield call(analytics.track, { trackingIntent: onMyActionTrackingIntent });
  } catch (error) {
    Sentry.captureException(error);
  }
}

export function* watchMyAction(options) {
  yield takeLatest(MyActionTypes.MY_ACTION, onMyAction, options);
}

export default function* trackingSaga(options = {}) {
  yield all([
    call(watchMyAction, options),
  ]);
}

Alternatively, we can pass analytics.track a trackingEl (which is a DOMElement), and optionally an event so it can manage cancellation (preventDefault) and await completion of the tracking call before redirecting.

Although less direct, this approach may be preferred to resemble other declarative trackers by also storing the tracking data in the DOM, which hopefully helps towards faster debugging and QA experiences.

NOTE: Event cancellation is only needed for IE 11. All other supported browsers implement Beacon API, which is a non-blocking interface for POST requests, so @doodle/tracking does not need to await before redirecting.

import { getTrackingDataAttrs } from '@doodle/tracking';
import { analytics } from '../../analytics';

const MyCTA = () => {
  const trackingIntent = {
    options: {
      // we disable the global listener from automatically tracking this intent, so we can track it conditionally
      autoTracking: false,
    }
    track: {
      event: 'Click Trial Button',
      type: 'user Interaction',
      properties: {
        category: 'Pricing Page',
        description: 'User clicks on "Start free Trial"',
        'Trial Premium Plan': 'Starter',
      },
    },
  };

  const handleClick = event => {
    if (someCondition) {
      analytics.track({ trackingEl: event.currentTarget, event });
    }
  }

  return (
    <Button onClick={handleClick}>
      <span>Start free Trial</span>
    </Button>
  );
}

5. Consent Management API

After the initial prompt for consent, the entire consent management process is invisible to the user -- the prompt will not be shown on every page load. By law however, a user must be able to change their decision at a later point in time (even after consent has been granted in the past).

Thus each application integrating @doodle/tracking must offer the ability for the user to revise the consent.

Applications can (re-)open the consent management preferences screen by calling the API.showConsentPreferences() method. Once the user has changed their settings, @doodle/tracking automatically takes care of loading/unloading the correct trackers.

Additionally, the API.changeConsentPreferencesLanguage(locale) method can be used to change the language of OneTrust provided prompts (in the case of an SPA).

If your application has advanced needs for Consent Management, for example it needs to wait for consent before loading ad vendor implementation, a consent change listener can be registered. Registration of such a listener can be done with a call to API.registerConsentChangeListener(handler). The handler argument is a function with the following signature:

function (activeGroups: string[], context: {
   isInitialized: boolean, // true if OneTrust was installed and initialized
   userDidInteract: boolean, // true if the user did interact with OneTrust UI/prompts
   rawActiveGroups: string // the raw string containing serialised active group Ids as returned by OneTrust
})

This handler is called every time the consent is updated, as well as after the initial load of OneTrust (on page load).

The currently active groups/consents can also be obtained without registering a change listener. Just call API.getConsents(). This will return the consent category IDs as an array.

If for whatever reason you need to check if a given category Id has consent, you can call API.hasConsent(consentCategoryId).

6. Real world examples

7. Mapping to destination services

7.1 Amplitude

Reference:

7.1.1 track

A Doodle Data Layer's track field is dispatched as an Amplitude's AmplitudeClient.logEvent call:

Doodle Data Layer Amplitude
event eventName
type properties['Event Type']
properties.category properties.EventCategory
properties.description properties.EventDescription
properties['Custom property'] properties['Custom property']

7.1.2 identify

A Doodle Data Layer's identify field is dispatched as one or multiple amplitude-js' Identify.set call:

Doodle Data Layer Amplitude
event eventName
properties properties

7.1.3 page

No mappings for Amplitude, which does not support page calls directly.

7.2. Google Analytics

Reference:

7.2.1 track

A Doodle Data Layer's track field is dispatched as a Google Analytics' event. Note value should always be a number:

Doodle Data Layer Google Analytics Short description
event eventAction Mandatory, string
type eventCategory Optional, string
properties.category eventPage Optional, string
properties.label eventLabel Optional, string
properties.name eventName Optional, string
properties.value eventValue Optional, must be a number

7.2.2 identify

A Doodle Data Layer's identify command has its properties keys pushed to Google Tag Manager as user GTM Data Layer Variables.

NOTE

Such user properties must be pre-created in Google Tag Manager or the push will fail:

Doodle Data Layer Google Analytics
userId N/A
properties.mandatorId user.mandatorId
properties.userCurrency user.userCurrency
properties.userIsTrial user.userIsTrial
properties.notAddedToGTM N/A

7.2.3 page

A Doodle Data Layer's page field is dispatched as a Google Analytics' page. For real page views:

Doodle Data Layer Google Analytics
properties.category + properties.name title
properties.location collected automatically by GA client
properties.page collected automatically by GA client

For "virtual page views", i.e. those where navigation occurs without a location bar URL change:

Doodle Data Layer Google Analytics
properties.path page
properties.title title
properties.url location

8. Development of this package

This library was architected to be more developer-friendly by not using Sagas and not distributing transpiled source code, which allows for a regular usage of yarn link.

9. Publishing of this package

This library is published to the registry using Tagflow.

9.1 Publish release candidate

git tag pub.1.0.0-rc.0
git push origin pub.1.0.0-rc.0

9.2 Publish release

git tag pub.1.0.0
git push origin pub.1.0.0