@technoapple/ga4
Version:
TypeScript Node.js library to support GA4 analytics.
91 lines (74 loc) • 2.85 kB
text/typescript
import { GA4Plugin, SendFunction, EventTrackerOptions } from '../types/plugins';
import { delegate, DelegateHandle } from '../helpers/delegate';
function kebabToSnake(str: string): string {
return str.replace(/-/g, '_');
}
function getAttributeParams(element: Element, prefix: string): Record<string, unknown> {
const params: Record<string, unknown> = {};
const reservedSuffixes = ['on', 'event-name'];
for (let i = 0; i < element.attributes.length; i++) {
const attr = element.attributes[i];
if (!attr.name.startsWith(prefix)) continue;
const suffix = attr.name.slice(prefix.length);
if (reservedSuffixes.includes(suffix)) continue;
params[kebabToSnake(suffix)] = attr.value;
}
return params;
}
/**
* Declarative event tracking via HTML `data-ga4-*` attributes.
*
* Listens for DOM events on elements with `data-ga4-on` attributes
* and sends GA4 events based on attribute values.
*
* @example
* ```html
* <button
* data-ga4-on="click"
* data-ga4-event-name="video_play"
* data-ga4-video-title="My Video">
* Play
* </button>
* ```
*/
export class EventTracker implements GA4Plugin {
private delegates: DelegateHandle[] = [];
private send: SendFunction;
private events: string[];
private attributePrefix: string;
private hitFilter?: EventTrackerOptions['hitFilter'];
constructor(send: SendFunction, options?: EventTrackerOptions) {
this.send = send;
this.events = options?.events ?? ['click'];
this.attributePrefix = options?.attributePrefix ?? 'data-ga4-';
this.hitFilter = options?.hitFilter;
const selector = `[${this.attributePrefix}on]`;
this.events.forEach((eventType) => {
const handle = delegate(
document,
eventType,
selector,
(event, element) => this.handleEvent(event, element),
{ composed: true, useCapture: true }
);
this.delegates.push(handle);
});
}
private handleEvent(event: Event, element: Element): void {
const prefix = this.attributePrefix;
const onAttr = element.getAttribute(`${prefix}on`);
if (onAttr !== event.type) return;
const eventName = element.getAttribute(`${prefix}event-name`) || event.type;
let params = getAttributeParams(element, prefix);
if (this.hitFilter) {
const filtered = this.hitFilter(params, element, event);
if (filtered === null) return;
params = filtered;
}
this.send(eventName, params);
}
remove(): void {
this.delegates.forEach((d) => d.destroy());
this.delegates = [];
}
}