UNPKG

@technoapple/ga4

Version:

TypeScript Node.js library to support GA4 analytics.

105 lines (89 loc) 3.99 kB
import { GA4Plugin, SendFunction, PageVisibilityTrackerOptions } from '../types/plugins'; import { isSessionExpired, updateSessionTimestamp } from '../helpers/session'; /** * Tracks how long a page is in the visible state vs. hidden (background tab). * * Sends a `page_visibility` event each time the visibility state changes, * reporting how long the page was in the previous state. * Optionally sends a new `page_view` when the page becomes visible * again after the session timeout has elapsed. */ export class PageVisibilityTracker implements GA4Plugin { private send: SendFunction; private sendInitialPageview: boolean; private sessionTimeout: number; private eventName: string; private hitFilter?: PageVisibilityTrackerOptions['hitFilter']; private lastChangeTime: number; private isVisible: boolean; private boundVisibilityChange: () => void; private boundBeforeUnload: () => void; constructor(send: SendFunction, options?: PageVisibilityTrackerOptions) { this.send = send; this.sendInitialPageview = options?.sendInitialPageview ?? false; this.sessionTimeout = options?.sessionTimeout ?? 30; this.eventName = options?.eventName ?? 'page_visibility'; this.hitFilter = options?.hitFilter; this.lastChangeTime = Date.now(); this.isVisible = document.visibilityState === 'visible'; this.boundVisibilityChange = this.onVisibilityChange.bind(this); this.boundBeforeUnload = this.onBeforeUnload.bind(this); document.addEventListener('visibilitychange', this.boundVisibilityChange); window.addEventListener('beforeunload', this.boundBeforeUnload); updateSessionTimestamp('pageVisibility'); if (this.sendInitialPageview && this.isVisible) { this.send('page_view', { page_path: location.pathname, page_location: location.href, page_title: document.title, }); } } private onVisibilityChange(): void { const now = Date.now(); const duration = now - this.lastChangeTime; const previousState = this.isVisible ? 'visible' : 'hidden'; let params: Record<string, unknown> = { visibility_state: previousState, visibility_duration: duration, page_path: location.pathname, }; if (this.hitFilter) { const filtered = this.hitFilter(params); if (filtered === null) { this.lastChangeTime = now; this.isVisible = document.visibilityState === 'visible'; return; } params = filtered; } this.send(this.eventName, params); // If page becomes visible again, check session expiry if (document.visibilityState === 'visible' && !this.isVisible) { if (isSessionExpired('pageVisibility', this.sessionTimeout)) { this.send('page_view', { page_path: location.pathname, page_location: location.href, page_title: document.title, }); } updateSessionTimestamp('pageVisibility'); } this.lastChangeTime = now; this.isVisible = document.visibilityState === 'visible'; } private onBeforeUnload(): void { if (this.isVisible) { const duration = Date.now() - this.lastChangeTime; this.send(this.eventName, { visibility_state: 'visible', visibility_duration: duration, page_path: location.pathname, }); } } remove(): void { document.removeEventListener('visibilitychange', this.boundVisibilityChange); window.removeEventListener('beforeunload', this.boundBeforeUnload); } }