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. Initial setup
  3. Declarative API
  4. Imperative API
  5. Real world examples
  6. Mapping to destination services
    1. Amplitude
    2. Google Analytics
  7. Development of this package
  8. 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
category string
name string The name of the page you are tracking
properties Object Custom event properties
properties.path string
properties.title string
properties.url string
properties.referrer string

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

options (Object)

Property Type Description
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
autoTracking boolean Default: true. Controls click handler behavior for a specific tracked item

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
  1. Install with yarn add @doodle/tracking

  2. 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 { API } from '@doodle/tracking';

export const analytics = API.init({
  clientId: 'web-scheduling-experience', // or web-billing (following kubernetes service name)
  env: {
    amplitudeApiKey: process.env.AMPLITUDE_API_KEY, // needed only if Amplitude service is enabled
    doodleEnv: process.env.DOODLE_ENV, // if Google Analytics service is enabled
    nodeEnv: process.env.NODE_ENV, // if Google Analytics service is enabled
    svcDataLayerApi: process.env.SVC_DATA_LAYER_API, // if Doodle Data Layer service is enabled
  },
});
  1. Start tracking!

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

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.

Then the analytics.track method can be called in two possible ways. Either pass it an object with a trackingIntent key, as below:

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

Another equivalent imperative call, now 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 = {
      track: {
        event: 'Click Trial Button',
        type: 'user Interaction',
        properties: {
          category: 'Pricing Page',
          description: 'User clicks on "Start free Trial"',
          'Trial Premium Plan': 'Starter',
        },
      },
    };
    analytics.track({ trackingIntent });
  }

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

Yet another (equivalent) example, in a saga worker:

import { apply } 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 apply(analytics, 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),
  ]);
}

Or alternatively pass analytics.track a trackingEl (DOMElement), and an event so it can manage event cancellation (preventDefault) and await completion of tracking network request 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: {
      autoTracking: false, // this disables the global listener from automatically tracking this intent
    }
    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 => {
    analytics.track({ trackingEl: event.currentTarget, event });
  }

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

NOTE: If both a trackingIntent and a trackingEl (and event) are provided to analytics.track, the former is preferred and will be used instead of the element's data attributes for dispatching the tracking data.

5. Real world examples

6. Mapping to destination services

6.1 Amplitude

6.1.1 track

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

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

6.1.2 identify

A Doodle Data Layer's identify field is dispatched as an Amplitude's Identify.set call:

Doodle Data Layer Amplitude
event eventName
properties properties
trackingId deviceId

6.1.3 page

No mappings for Amplitude.

6.2. Google Analytics

6.2.1 track

A Doodle Data Layer's track field is dispatched as a Google Analytics' event:

Doodle Data Layer Google Analytics
event eventAction
type eventCategory
properties.category eventCategory
properties.label eventLabel
properties.value eventValue
context.page.eventPage eventPage

6.2.2 identify

No mappings for Google Analytics.

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

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

8. Publishing of this package

This library is published to the registry using Tagflow.

8.1 Publish release candidate

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

8.2 Publish release

git tag pub.1.0.0
git push origin pub.1.0.0