Contextual and Consistent Analytic Events in React

Using react-event-tracking to build context and trigger events

React 16 context provides a great foundation to build an eventing framework. We used that capability to create react-event-tracking, which enables applications and components to iteratively build event information and trigger events where the user activity occurs. This allows component enablement ahead of application enablement and validation of triggered events during testing, leading to consistent eventing throughout applications.

History

At HomeAway, we use a custom-built user analytic event tracking platform. One of its features is a client-side API to trigger events as well as the schema defining those events. The development team uses these features to track user activity through applications by triggering events at various points in the application DOM hierarchy.

Mockup of analytic events

While this custom tracking platform has served us well, we are also working hard on a common React component ecosystem (buttons, calendars, inputs, etc.) across our applications. This has greatly reduced unnecessary duplication of code and ensures consistency in experience as users move through the site. However, as a company that uses data to drive decisions, we have experienced some pain points around triggering analytic events and using a common component ecosystem.

Pain Point #1: Common components don’t know full context

Common components know when an event occurs (such as a mouse click or selection), but they do not know all the details necessary to satisfy the required and optional fields (app name, location on the page, property ids, etc.) of a user analytic event. This makes it painful to trigger a schema compliant event. This is typically worked around by either passing required/optional fields all the way down through the component hierarchy (yuck!), using globals (😱), or by the common components implementing callbacks and relying on the consuming application to trigger the event in the implemented function. (Spoiler alert: None of these are desirable.)

Pain Point #2: Inconsistent event triggering

Working around Pain Point #1 through callbacks places the responsibility on the consuming application of the component to trigger the event. This leads to inconsistency in event triggering as there is no guarantee that every application will trigger the event.

A library solution

With the above pain points in mind, we set out to build a solution that would allow applications to iteratively build the information for an event and allow the lowest-level component to trigger the event with the full set of details. Fortunately, React 16 formalized the concept of context:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Using React 16 context as a foundation, we’ve built react-event-tracking. The react-event-tracking repository provides functionality to trigger events while allowing applications to specify additional details that are automatically included in the triggered event. This helps to ensure consistency in event triggering across consuming applications as the component itself is triggering the event instead of relying on the application.

This article goes into quite a bit of the detail around the implementation and usage of the components available in react-event-tracking. If you want to avoid the nitty gritty details, the overview is:

Event triggering with react-event-tracking
  1. Applications use a TrackingProvider to define the event triggering implementation and wrap components that trigger events with additional fields and options that will be merged into the triggered event.
  2. Components make use of TrackingContext or TrackingTrigger to trigger events which will automatically merge the fields and options specified at higher levels in the DOM through one or more TrackingProvider components.

TrackingProvider

The TrackingProvider component allows an application to define the event trigger implementation and incrementally build the fields and options for events that will be triggered from nested components. The TrackingProvider is a React 16 context provider intended as a generic solution that does not require the use of a specific analytic event tracking library.

import {TrackingProvider} from '@vrbo/react-event-tracking';
const defaultFields = {location: 'top-right'};
const defaultOptions = {asynchronous: true};
const customTrigger = (event, fields, options) => {
// Implement custom event tracking.
}
function App(props) {
return (
<TrackingProvider
fields={defaultFields}
options={defaultOptions}
trigger={customTrigger}
>
// Events triggered by the Calendar component
// will use the context specified above.
<Calendar/>
</TrackingProvider>
);
}

Trigger implementation

The application is responsible for providing an implementation of the trigger method via the trigger property. The provided implementation is expected to have the following signature:

trigger(event, fields, options)

Where:

  • event — The name of the event to trigger (String).
  • fields — The required and optional fields for the event (Object of string values).
  • options — Options for the custom trigger implementation to use when triggering the event. (Object — implementation specific)

TrackingProvider nesting

Multiple TrackingProvider components can be nested in order to iteratively enhance the configuration where the information is known. For example, consider a scenario where the Calendar component from the first TrackingProvider example above is making use of a component named EventButton which triggers a generic.click event. An application can build the fields needed for the generic.click by nesting TrackingProvider wrappers in the areas of code that know the related information.

Nested component providing additional fields

import {TrackingProvider} from '@vrbo/react-event-tracking';
const buttonFields = {location: 'top-right'};
function CalendarButton(props) {
return (
<TrackingProvider fields={buttonFields}>
<EventButton>{'Click Me'}</EventButton>
</TrackingProvider>
);
}

When EventButton triggers the generic.click event, the trigger handler will be sent the combined set of fields from both the CalendarButton and application TrackingProvider instances.

TrackingContext

While the TrackingProvider component is used to incrementally build the fields and options for an event and define the trigger implementation, the TrackingContext module is used to trigger the analytic event. TrackingContext is a React 16 context object that is used to store the value of fields and options and provides access to the trigger method to trigger events. TrackingProvider and TrackingContext together enable an application to progressively build the fields and options and then trigger events at the lowest level. Once configured as shown, the component can then access the trigger method via this.context.trigger.

import React, {Component} from 'react';
import {TrackingContext} from '@vrbo/react-event-tracking'
class MyComponent extends Component {
static contextType = TrackingContext;
    handleClick() {
this.context.trigger(`generic.click`);
}
...
}

Alternatively, if the application has been upgraded to React >16.8.0, the useContext hooks api can be used to get access to the trigger method.

TrackingTrigger

The TrackingTrigger component allows an application to declaratively trigger a tracking event. It is used in conjunction with the TrackingProvider component to trigger events in a standardized way. Specify the desired event name, fields and options to include when the event is triggered. The event will be triggered with a merge of the specified fields and options and the current context when the component's componentDidMount is invoked.

import {TrackingTrigger} from '@vrbo/react-event-tracking';
const eventFields = {
location: 'searchbar',
name: 'SomeComponent'
};
const eventOptions = {asynchronous: true};
function SomeComponent(props) {
return (
...
<TrackingTrigger
event={'visible'}
fields={eventFields}
options={eventOptions}
/>
);
}

Event validation

At HomeAway, our user analytic event tracking platform has historically not rejected events that are invalid according to the schema. This strategy streamlines the event triggering process and prevents errors from showing up in the applications. However, it has led to bad data entering the system, which must be cleaned prior to analysis. To promote schema-compliant event triggering, we have created a wrapper component around TrackingProvider that provides an implementation of the trigger method which validates event requests against our analytic event schema. The scaffold for setting up a validation provider could be as simple as below:

import React, {Component} from 'react';
import {TrackingProvider} from '@vrbo/react-event-tracking';
class TrackingProviderValidator extends Component {
  trigger = (event, fields = {}, options = {}) => {
// Add implementation specific code to validate the event,
// fields and options against your schema. Throw an exception
// or use some other means to cause unit tests to fail if
// not valid
}
  render() {        
return (
<TrackingProvider
{...this.props}
trigger={this.trigger}
>
{children}
</TrackingProvider>
);
}
}

We then use this validation provider in our unit tests to ensure that the events being triggered by a component are valid according to the schema.

Caveats

  1. The use of contextType in a React component requires react: ^16.6.0.
  2. Prior to React 16.8.0, it was not possible for a component to use multiple contextType definitions. If a component needs to consume multiple contexts, use the useContext hooks API made available in React 16.8.0.
  3. If a TrackingProvider with a trigger implementation is not defined somewhere in the hierarchy, the trigger API will essentially be a no-op. This allows components to be enabled to trigger events regardless of whether or not the application is configured to trigger them.
  4. We recommend that property values for the TrackingProvider be defined in state or constant variables instead of building the values dynamically on every render. If the values are constructed during the render, it will cause a forced re-rendering of all consumers of the context that are descendants of the provider, even if the consumer’s shouldComponentUpdate bails out. Following a pattern of defining property values as constants or via state will prevent unnecessary renders of children context consumers.

Summary

Building a common suite of components in React that is used throughout a wide range of applications saves developer time through code reuse and also encourages consistency in look and feel. The user analytic data produced by those components should also be consistent, however, it has been difficult to achieve and enforce that consistency when each application determines when events are triggered. react-event-tracking provides a means through React 16 context for applications to provide the necessary details but allow the common components to trigger the events where they occur. We hope that other teams find react-event-tracking useful and look forward to your feedback and contributions to the open source project.