Google Analytics Events with Gatsby and TypeScript
Context
If you track user behavior in Gatsby, TypeScript can do more than catch obvious mistakes. It can also make your analytics schema explicit so event names stay consistent across the app.
The pattern in this post is simple:
- Define the finite set of categories, actions, and labels in one file.
- Wrap
trackCustomEventso the rest of the app uses your typed helper. - Allow a small escape hatch for labels that must be generated at runtime.
npm install gatsby-plugin-google-analytics
This post focuses on the event schema and tracking code. It assumes you already have Google Analytics wired up in Gatsby.
Model Analytics Events with Types
A typical analytics event has three parts:
- Category: what part of the product was involved
- Action: what the user did
- Label: which specific item or context was involved
Instead of scattering string literals throughout the codebase, I keep those values in a single analytics.ts module.
export enum AnalyticsCategory {
Themes = 'Themes',
Newsletter = 'Newsletter',
Blog = 'Blog',
Feedback = 'Feedback',
Contact = 'Contact',
}
export enum AnalyticsAction {
Click = 'Click',
Subscribe = 'Subscribe',
Toggle = 'Toggle',
SendEmail = 'Send Email',
Like = 'Like',
Dislike = 'Dislike',
Dismiss = 'Dismiss',
}
export enum AnalyticsLabel {
Light = 'Light',
Dark = 'Dark',
Green = 'Green',
Blue = 'Blue',
Red = 'Red',
}
export interface AnalyticsEvent {
category: AnalyticsCategory;
action: AnalyticsAction;
label?: AnalyticsLabel | string;
}
That interface gives me a single shape for every event. Most labels come from the enum, but some need runtime data such as a pathname or page title.
Use the Typed Schema at the Call Site
Here is a simple example of tracking a click on a button. The event is still flexible, but the category and action now come from a fixed set of values.
import {useLocation} from '@reach/router';
import {trackCustomEvent} from 'gatsby-plugin-google-analytics';
import {
AnalyticsAction,
AnalyticsCategory,
AnalyticsEvent,
} from '@utils/analytics';
function Button() {
const {pathname} = useLocation();
return (
<button
type="button"
onClick={() => {
trackCustomEvent({
category: AnalyticsCategory.Feedback,
action: AnalyticsAction.SendEmail,
label: pathname,
});
}}
>
Click
</button>
);
}
The main benefit here is consistency. If a new button is added later, it is much harder for someone to invent a new string like Send mail, send-email, or email sent and silently split the analytics data.
Add a Thin Wrapper
gatsby-plugin-google-analytics already exposes trackCustomEvent, but the plugin type definitions do not know about my own AnalyticsEvent interface. A tiny wrapper keeps the rest of the app on the typed API I control.
import {trackCustomEvent} from 'gatsby-plugin-google-analytics';
export function track(event: AnalyticsEvent) {
trackCustomEvent(event);
}
Once that wrapper exists, components can depend on track() instead of importing the plugin directly.
import {useLocation} from '@reach/router';
import {
AnalyticsAction,
AnalyticsCategory,
AnalyticsEvent,
track,
} from '@utils/analytics';
function Button() {
const {pathname} = useLocation();
return (
<button
type="button"
onClick={() => {
const event: AnalyticsEvent = {
category: AnalyticsCategory.Feedback,
action: AnalyticsAction.SendEmail,
label: pathname,
};
track(event);
}}
>
Click
</button>
);
}
Why This Helps
This setup keeps analytics readable and maintainable:
- Event values are centralized.
- The compiler catches typos in category and action names.
- Runtime labels are still possible when static values are not enough.
- The rest of the app stays unaware of the plugin’s internal types.
That is the main advantage of using TypeScript for analytics: the schema becomes part of the code, not just an informal naming convention.