feat: optional GA4 page view tracking

This commit is contained in:
karilint
2026-04-23 10:45:20 +03:00
parent 8d67a67140
commit 7dd0bfcd79
6 changed files with 78 additions and 1 deletions
+4
View File
@@ -39,3 +39,7 @@ VITE_RUNNING_ENV: dev
VITE_BACKEND_URL: http://localhost:4000 VITE_BACKEND_URL: http://localhost:4000
VITE_HMR_PORT: 5173 VITE_HMR_PORT: 5173
# Optional: enables Google Analytics (GA4) page-view tracking in the frontend when set.
# Disabled by default, and never runs in `VITE_RUNNING_ENV: dev`.
VITE_GA4_MEASUREMENT_ID: ""
+7
View File
@@ -67,6 +67,13 @@ Some DB fields use MariaDB `DECIMAL` and are represented as `Decimal` in Prisma-
- Prefer explicit parsing/conversion at usage points if numeric operations are required. - Prefer explicit parsing/conversion at usage points if numeric operations are required.
- `estimate_temp` additionally has validator bounds to avoid DB errors from out-of-range values (`-999.9` to `999.9`). - `estimate_temp` additionally has validator bounds to avoid DB errors from out-of-range values (`-999.9` to `999.9`).
**Analytics (optional)**
The frontend can optionally send GA4 page-view events if configured.
- Set `VITE_GA4_MEASUREMENT_ID` to your GA4 measurement id (for example `G-XXXXXXXXXX`).
- Tracking is disabled by default and never runs when `VITE_RUNNING_ENV` is `dev`.
**Detailed documentation for editingComponents** **Detailed documentation for editingComponents**
- [RadioSelector](./components/editingComponents/RadioSelector.md) - [RadioSelector](./components/editingComponents/RadioSelector.md)
+9 -1
View File
@@ -1,11 +1,19 @@
import { Container } from '@mui/material' import { Container } from '@mui/material'
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
import { Outlet } from 'react-router-dom' import { Outlet, useLocation } from 'react-router-dom'
import { NavBar } from './components/NavBar' import { NavBar } from './components/NavBar'
import { Notification, NotificationContextProvider } from './components/Notification' import { Notification, NotificationContextProvider } from './components/Notification'
import { Footer } from './Footer' import { Footer } from './Footer'
import { useEffect } from 'react'
import { trackPageView } from './util/analytics'
const App = () => { const App = () => {
const location = useLocation()
useEffect(() => {
trackPageView(`${location.pathname}${location.search}`)
}, [location.pathname, location.search])
return ( return (
<NotificationContextProvider> <NotificationContextProvider>
<Notification /> <Notification />
+1
View File
@@ -1,3 +1,4 @@
export const ENV = 'dev' export const ENV = 'dev'
export const BACKEND_URL = 'http://localhost' export const BACKEND_URL = 'http://localhost'
export const ENABLE_WRITE = true export const ENABLE_WRITE = true
export const GA4_MEASUREMENT_ID = undefined
+56
View File
@@ -0,0 +1,56 @@
import { ENV, GA4_MEASUREMENT_ID } from './config'
declare global {
interface Window {
dataLayer?: unknown[]
gtag?: (...args: unknown[]) => void
}
}
const canTrack = () => {
if (ENV === 'dev') return false
if (!GA4_MEASUREMENT_ID) return false
// Honor "Do Not Track" signals when present.
const win = window as Window & { doNotTrack?: string }
const dnt = navigator.doNotTrack === '1' || win.doNotTrack === '1'
if (dnt) return false
return true
}
let initialized = false
const ensureGtagLoaded = () => {
if (initialized) return
if (!canTrack()) return
if (typeof document === 'undefined') return
const srcId = 'ga4-gtag-src'
if (!document.getElementById(srcId)) {
const script = document.createElement('script')
script.id = srcId
script.async = true
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(GA4_MEASUREMENT_ID!)}`
document.head.appendChild(script)
}
window.dataLayer = window.dataLayer ?? []
window.gtag =
window.gtag ??
((...args: unknown[]) => {
window.dataLayer?.push(args)
})
window.gtag('js', new Date())
window.gtag('config', GA4_MEASUREMENT_ID, { send_page_view: false, anonymize_ip: true })
initialized = true
}
export const trackPageView = (path: string) => {
if (!canTrack()) return
ensureGtagLoaded()
window.gtag?.('event', 'page_view', { page_path: path })
}
+1
View File
@@ -1,3 +1,4 @@
export const ENV = import.meta.env.VITE_RUNNING_ENV as 'dev' | 'staging' | 'prod' export const ENV = import.meta.env.VITE_RUNNING_ENV as 'dev' | 'staging' | 'prod'
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL as string export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL as string
export const ENABLE_WRITE = import.meta.env.VITE_ENABLE_WRITE === 'true' export const ENABLE_WRITE = import.meta.env.VITE_ENABLE_WRITE === 'true'
export const GA4_MEASUREMENT_ID = import.meta.env.VITE_GA4_MEASUREMENT_ID as string | undefined