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.
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:
- 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. - Components make use of
TrackingContext
orTrackingTrigger
to trigger events which will automatically merge the fields and options specified at higher levels in the DOM through one or moreTrackingProvider
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
- The use of
contextType
in a React component requiresreact: ^16.6.0
. - 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 theuseContext
hooks API made available in React 16.8.0. - If a
TrackingProvider
with atrigger
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. - 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’sshouldComponentUpdate
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.