Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Every event automatically receives `page_location`, `page_path`, `page_title`, `

### `page_view`

Fires on initial load (auto from gtag config) and on every SPA navigation.
Fires once on initial load and once per SPA navigation, sent only by `<PageViewTracker />` in `src/routes/__root.tsx`. The gtag config uses `send_page_view: false`, and the GA4 web stream must keep enhanced measurement's "page changes based on browser history events" disabled; otherwise every navigation is counted twice.

| Prop | Type | Notes |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
Expand Down
29 changes: 23 additions & 6 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ const GOOGLE_ANALYTICS_ID = 'G-JMT1Z50SPS'
const GOOGLE_ANALYTICS_PROXY_PREFIX = '/_a'
const GOOGLE_ANALYTICS_SCRIPT_SRC = `${GOOGLE_ANALYTICS_PROXY_PREFIX}/gtag.js`
const THEME_BOOTSTRAP = `(function(){try{var t=localStorage.getItem('theme')||'auto';var v=['light','dark','auto'].includes(t)?t:'auto';var r=v==='auto'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):v;if(document.documentElement){document.documentElement.classList.add(r);if(v==='auto')document.documentElement.classList.add('auto');document.documentElement.style.colorScheme=r}}catch(e){if(document.documentElement){var r=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';document.documentElement.classList.add(r,'auto');document.documentElement.style.colorScheme=r}}})()`
const GOOGLE_ANALYTICS_BOOTSTRAP = `(function(){var id='${GOOGLE_ANALYTICS_ID}';var src='${GOOGLE_ANALYTICS_SCRIPT_SRC}';window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){window.dataLayer.push(arguments)};window.gtag('js',new Date());window.gtag('config',id,{transport_url:window.location.origin+'${GOOGLE_ANALYTICS_PROXY_PREFIX}'});var loaded=false;var load=function(){if(loaded)return;var parent=document.head||document.documentElement;if(!parent){window.setTimeout(load,100);return}loaded=true;var script=document.createElement('script');script.async=true;script.src=src;script.setAttribute('data-ga-loader','true');parent.appendChild(script)};if(typeof window.requestIdleCallback==='function'){window.requestIdleCallback(load,{timeout:3000});return}if(document.readyState==='complete'){window.setTimeout(load,1500);return}window.addEventListener('load',function(){window.setTimeout(load,1500)},{once:true})})();`
// Page views are sent only by <PageViewTracker /> (initial load and SPA
// navigations), so the gtag config sets send_page_view: false. The guard makes
// the bootstrap idempotent: without it the head script can execute twice on a
// single load, sending a second config (and a second page_view) and injecting
// a second gtag.js loader.
const GOOGLE_ANALYTICS_BOOTSTRAP = `(function(){if(window.__tanstackGaBootstrapped)return;window.__tanstackGaBootstrapped=true;var id='${GOOGLE_ANALYTICS_ID}';var src='${GOOGLE_ANALYTICS_SCRIPT_SRC}';window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){window.dataLayer.push(arguments)};window.gtag('js',new Date());window.gtag('config',id,{send_page_view:false,transport_url:window.location.origin+'${GOOGLE_ANALYTICS_PROXY_PREFIX}'});var loaded=false;var load=function(){if(loaded)return;var parent=document.head||document.documentElement;if(!parent){window.setTimeout(load,100);return}loaded=true;var script=document.createElement('script');script.async=true;script.src=src;script.setAttribute('data-ga-loader','true');parent.appendChild(script)};if(typeof window.requestIdleCallback==='function'){window.requestIdleCallback(load,{timeout:3000});return}if(document.readyState==='complete'){window.setTimeout(load,1500);return}window.addEventListener('load',function(){window.setTimeout(load,1500)},{once:true})})();`
const DOCUMENT_CACHE_HEADERS = {
'Cache-Control': 'public, max-age=0, must-revalidate',
'Cloudflare-CDN-Cache-Control': 'no-store',
Expand Down Expand Up @@ -408,23 +413,35 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
)
}

/**
* The single source of `page_view` events: one on initial load and one per
* SPA navigation. gtag config runs with `send_page_view: false`, and GA4's
* enhanced-measurement "page changes based on browser history events" must
* stay disabled on the web stream, or navigations are counted twice.
*/
function PageViewTracker() {
const pagePath = useRouterState({
select: (s) => {
const pathname = s.resolvedLocation?.pathname || '/'
const search = s.resolvedLocation?.searchStr || ''
if (!s.resolvedLocation) {
return null
}

const pathname = s.resolvedLocation.pathname || '/'
const search = s.resolvedLocation.searchStr || ''

return `${pathname}${search}`
},
})
const hasTrackedInitialPage = React.useRef(false)
const lastTrackedPath = React.useRef<string | null>(null)

React.useEffect(() => {
if (!hasTrackedInitialPage.current) {
hasTrackedInitialPage.current = true
// Skip until the router has resolved a location, and never send the same
// path twice in a row (guards against the location resolving twice).
if (pagePath === null || lastTrackedPath.current === pagePath) {
return
}

lastTrackedPath.current = pagePath
trackPageView(pagePath)
}, [pagePath])

Expand Down
Loading