diff --git a/src/lib/analytics/gtag.ts b/src/lib/analytics/gtag.ts new file mode 100644 index 00000000..564d06ef --- /dev/null +++ b/src/lib/analytics/gtag.ts @@ -0,0 +1,64 @@ +// Google Analytics 4 (gtag.js) plumbing. +// +// SvelteKit is a single-page app after hydration, so GA's automatic page_view +// only fires for the very first server response. We disable that automatic hit +// (`send_page_view: false`) and emit a page_view ourselves on every client-side +// navigation. See Analytics.svelte for the lifecycle wiring. + +type GtagArgs = unknown[]; + +declare global { + interface Window { + dataLayer?: GtagArgs[]; + gtag?: (...args: GtagArgs) => void; + } +} + +// The id we have already injected, so initAnalytics is idempotent and +// setAnalyticsUser knows which config to address. +let activeId: string | null = null; + +/** + * Inject gtag.js for `measurementId` and prime the data layer. Browser-only and + * idempotent — safe to call from afterNavigate on every navigation. The initial + * automatic page_view is disabled; callers emit page_view via trackPageView. + */ +export function initAnalytics(measurementId: string, options: { userId?: string | null } = {}): void { + if (typeof window === 'undefined' || !measurementId || activeId === measurementId) { + return; + } + activeId = measurementId; + + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`; + document.head.appendChild(script); + + window.dataLayer = window.dataLayer ?? []; + // gtag forwards its raw argument list onto the data layer; gtag.js reads each + // entry by index/length, so pushing the rest array behaves like `arguments`. + window.gtag = function gtag(...args: GtagArgs) { + window.dataLayer!.push(args); + }; + window.gtag('js', new Date()); + window.gtag('config', measurementId, { + send_page_view: false, + ...(options.userId ? { user_id: options.userId } : {}) + }); +} + +/** Emit a page_view for the current document/location. No-op until initAnalytics has run. */ +export function trackPageView(path: string): void { + if (typeof window === 'undefined' || !window.gtag) return; + window.gtag('event', 'page_view', { + page_path: path, + page_location: window.location.href, + page_title: document.title + }); +} + +/** Align GA's user_id with the logged-in user (pass null on logout). No-op until initAnalytics has run. */ +export function setAnalyticsUser(userId: string | null): void { + if (typeof window === 'undefined' || !window.gtag || !activeId) return; + window.gtag('config', activeId, { user_id: userId }); +} diff --git a/src/lib/components/Analytics.svelte b/src/lib/components/Analytics.svelte new file mode 100644 index 00000000..ea40a9b1 --- /dev/null +++ b/src/lib/components/Analytics.svelte @@ -0,0 +1,39 @@ + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 23af5280..54a24cee 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -16,6 +16,7 @@ import { page } from '$app/state'; import OverviewDrawer from './OverviewDrawer.svelte'; import Sidebar from './Sidebar.svelte'; + import Analytics from '$lib/components/Analytics.svelte'; import { createAppContext, setAppContext } from '$lib/appContext.svelte'; import type { DeviceStatusSummary, RuleTemplateDto } from '$lib/api/api.dtos'; import type { IJWT } from '$lib/interfaces/jwt.interface'; @@ -114,6 +115,8 @@ + +