89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
|
|
type GtagFn = (...args: unknown[]) => void;
|
||
|
|
|
||
|
|
const TEXT_LIMIT = 80;
|
||
|
|
|
||
|
|
function gtag(): GtagFn | null {
|
||
|
|
if (typeof window === 'undefined') return null;
|
||
|
|
const fn = (window as unknown as { gtag?: GtagFn }).gtag;
|
||
|
|
return typeof fn === 'function' ? fn : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function trackPageView(path: string, title: string): void {
|
||
|
|
const send = gtag();
|
||
|
|
if (!send) return;
|
||
|
|
send('event', 'page_view', {
|
||
|
|
page_path: path,
|
||
|
|
page_title: title,
|
||
|
|
page_location: window.location.href
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function getLabel(el: HTMLElement): string {
|
||
|
|
const aria = el.getAttribute('aria-label');
|
||
|
|
if (aria) return aria.trim().slice(0, TEXT_LIMIT);
|
||
|
|
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim();
|
||
|
|
if (text) return text.slice(0, TEXT_LIMIT);
|
||
|
|
const title = el.getAttribute('title');
|
||
|
|
if (title) return title.trim().slice(0, TEXT_LIMIT);
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
function getLocation(el: HTMLElement): string {
|
||
|
|
const dataLoc = el.closest<HTMLElement>('[data-track-location]');
|
||
|
|
if (dataLoc?.dataset.trackLocation) {
|
||
|
|
return dataLoc.dataset.trackLocation.slice(0, TEXT_LIMIT);
|
||
|
|
}
|
||
|
|
const section = el.closest<HTMLElement>('section[id]');
|
||
|
|
if (section?.id) return section.id;
|
||
|
|
if (el.closest('header')) return 'header';
|
||
|
|
if (el.closest('footer')) return 'footer';
|
||
|
|
if (el.closest('nav')) return 'nav';
|
||
|
|
return 'body';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function initClickTracking(): () => void {
|
||
|
|
if (typeof window === 'undefined') return () => {};
|
||
|
|
|
||
|
|
const handler = (event: MouseEvent) => {
|
||
|
|
const send = gtag();
|
||
|
|
if (!send) return;
|
||
|
|
const target = event.target;
|
||
|
|
if (!(target instanceof Element)) return;
|
||
|
|
const interactive = target.closest<HTMLElement>('a, button, [role="button"]');
|
||
|
|
if (!interactive) return;
|
||
|
|
|
||
|
|
const isLink = interactive.tagName === 'A';
|
||
|
|
const label = getLabel(interactive);
|
||
|
|
const location = getLocation(interactive);
|
||
|
|
|
||
|
|
const params: Record<string, unknown> = {
|
||
|
|
element: isLink ? 'link' : 'button',
|
||
|
|
label,
|
||
|
|
location,
|
||
|
|
page_path: window.location.pathname
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isLink) {
|
||
|
|
const href = (interactive as HTMLAnchorElement).href;
|
||
|
|
if (href) {
|
||
|
|
params.link_url = href;
|
||
|
|
try {
|
||
|
|
const url = new URL(href, window.location.href);
|
||
|
|
params.outbound = url.hostname !== window.location.hostname;
|
||
|
|
} catch {
|
||
|
|
params.outbound = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const btn = interactive as HTMLButtonElement;
|
||
|
|
if (btn.type) params.button_type = btn.type;
|
||
|
|
if (btn.name) params.button_name = btn.name;
|
||
|
|
}
|
||
|
|
|
||
|
|
send('event', isLink ? 'link_click' : 'button_click', params);
|
||
|
|
};
|
||
|
|
|
||
|
|
document.addEventListener('click', handler, { capture: true });
|
||
|
|
return () => document.removeEventListener('click', handler, { capture: true });
|
||
|
|
}
|