Home

Strongly Typed Google Analytics Events with Typescript

For MealSpotter, we use Google Analytics Events to help us better understand how well Deals are performing, and bring that insight to our restaurant partners. Ensuring that our analytics data are clean helps us to work with our partners to bring even better deals to our users.

This post will go into how to define an Events schema using Typescript, using a simplified example of the method we use at MealSpotter.

Each event contains the following data:

  1. Category
  2. Action
  3. Label (optional)
  4. Value (optional)

While the label is optional, having the additional specificity really comes in handy. If you want to assign a monetary value to a particular event, then that field can be used as well.

The Code

This example uses a similar pattern to Redux with Typescript: You take a broad type and narrow it down to specific events, which are created via helper functions.

Setup

Let’s say we want to use events to track how often a user clicks on boxes of various colors. We have a simple component for a Box:

 0/* src/box.tsx */
 1import React from 'react';
 2// type Color = 'red' | 'green' | 'blue';
 3import Color from './color';
 4
 5type Props = {
 6  color: Color;
 7  onClick(): void;
 8};
 9
10export const Box: React.FC<Props> = ({ color, onClick }) => (
11  <div style={{
12    backgroundColor: color,
13    height: '10rem',
14    width: '30vw',
15    margin: '8px',
16    display: 'inline-block',
17    color: 'white',
18    textAlign: 'center',
19  }}
20  onClick={() => onClick()}
21  >
22    <p style={{
23      verticalAlign: 'middle',
24    }}>
25      Click me!
26    </p>
27  </div>
28);

And we render a box for each color in our App:

 0/* src/app.tsx */
 1import React from 'react';
 2import Box from './box';
 3import Color from './color';
 4
 5const App: React.FC = () => {
 6  const colors: Color[] = ['red', 'green', 'blue'];
 7
 8  function handleClick(color: Color): void {
 9    console.log(`color ${color} clicked`);
10  }
11
12  const boxes = colors.map((color) => (
13    <Box color={color}
14         key={color}
15         onClick={() => handleClick(color)}
16    />
17  ));
18
19  return (
20    <div>
21      { boxes }
22    </div>
23  )
24};
25
26export default App;

Implementing Events

Now that we have the components set up, we can move onto the events. Assuming we have a simple facade to interface with GA:

 0/* src/analytics-facade/i-analytics-facade.ts */
 1export interface IAnalyticsEvent {
 2  category: string;
 3  action: string;
 4  label?: string;
 5  value?: number;
 6}
 7
 8export interface IAnalyticsFacade<T extends IAnalyticsEvent> {
 9  sendEvent(event: T): void;
10}

Since we’re currently only tracking one “action” — clicking a box — our event type is pretty simple:

0/* src/events.ts */
1import { IAnalyticsEvent } from "./analytics-facade/i-analytics-facade";
2import Color from "./color";
3
4export type BoxClickEvent = IAnalyticsEvent & {
5  category: 'box',
6  action: 'click',
7  label: Color,
8};

On line 4 we’re taking the IAnalyticsEvent and narrowing it to specific types for category, action, and label. Now let’s write a helper function to create new events from colors:

 8/* src/events.ts */
 9// ...
10export const boxClick = (color: Color): BoxClickEvent => ({
11  category: 'box',
12  action: 'click',
13  label: color,
14});

Writing a simple class which can handle only our events ensures that only events which comply with our event schema are sent to GA:

0/* src/analytics-facade/analytics-facade.ts */
1import { IAnalyticsFacade } from "./i-analytics-facade";
2import { BoxClickEvent } from "../events";
3
4export class AnalyticsFacade implements IAnalyticsFacade<BoxClickEvent> {
5  public sendEvent(event: BoxClickEvent): void {
6    // In reality, this would be a call to GA
7    console.debug(event);
8  }
9}

Adding another event

Clicks are great, but normally there are multiple events which need to be tracked. Let’s add another event for when a box is displayed (an impression):

14/* src/events.ts */
15// ...
16export type BoxImpressionEvent = IAnalyticsEvent & {
17  category: 'box',
18  action: 'impression',
19  label: Color,
20};
21
22export const boxImpression = (color: Color): BoxImpressionEvent => ({
23  category: 'box',
24  action: 'impression',
25  label: color,
26});

We can wrap up all of our event types into a union type to use with our AnalyticsFacade:

26/* src/events.ts */
27// ...
28export type AppEvent = BoxClickEvent | BoxImpressionEvent;
2/* src/analytics-facade/analytics-facade.ts */
3// ...
4export class AnalyticsFacade implements IAnalyticsFacade<AppEvent> {
5  public sendEvent(event: AppEvent): void {
6    console.debug(event);
7  }
8}

Going beyond

As your events grow, it’s useful to split the event type definitions and event creators into their own modules. Even further down the line, defining common properties as an enum of strings, or consolidating common collections into their own intermediate types, can make the code more clear. A contrived example with these two events could look something like this:

 0/* src/events/types.ts */
 1import { IAnalyticsEvent } from "../analytics-facade/i-analytics-facade";
 2import Color from "../color";
 3
 4// Categories
 5export enum EventCategory {
 6  Box = 'box',
 7}
 8
 9// Actions
10export enum BoxAction {
11  Click = 'click',
12  Impression = 'impression',
13}
14
15type EventAction = BoxAction;
16
17// Labels
18type BoxLabel = Color;
19
20type EventLabel = BoxLabel;
21
22// Events
23/**
24 * The base event which can only be of a predefined `category`, `action`, and `label`
25 * the `label` narrowing may not fit all use cases
26 */
27export type AppEvent = IAnalyticsEvent & {
28  category: EventCategory,
29  action: EventAction,
30  label: EventLabel,
31}
32
33/**
34 * Events for the `Box` category
35 */
36type BoxEvent = AppEvent & {
37  category: EventCategory.Box,
38  action: BoxAction,
39  label: BoxLabel,
40};
41
42export type BoxClickEvent = BoxEvent & {
43  action: BoxAction.Click
44};
45
46export type BoxImpressionEvent = BoxEvent & {
47  action: BoxAction.Impression,
48};
 0/* src/events/creators.ts */
 1import { BoxClickEvent, BoxImpressionEvent, EventCategory, BoxAction } from "./types";
 2import Color from "../color";
 3
 4export const boxClick = (color: Color): BoxClickEvent => ({
 5  category: EventCategory.Box,
 6  action: BoxAction.Click,
 7  label: color,
 8});
 9
10export const boxImpression = (color: Color): BoxImpressionEvent => ({
11  category: EventCategory.Box,
12  action: BoxAction.Impression,
13  label: color,
14});

Conclusion

Ensuring that GA data is structured and conforming to a specific schema can be business critical, and can save having to painstakingly fix or throw out valuable data. Leveraging Typescript to ensure that code cannot send incorrect data is another line of defense which I can recommend.

Feedback, questions, and comments are always welcome! You can find me @[email protected]. Happy coding!