From 6c5c3bf2a63d76ee8cc05724c05ec6d63dd4fc6c Mon Sep 17 00:00:00 2001 From: profilesearch <222277199+profilesearch@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:22:48 -0700 Subject: [PATCH] fix: improve GA4 funnel instrumentation --- backend/test_deep_audit_regressions.py | 8 ++++ frontend/index.html | 60 ++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/backend/test_deep_audit_regressions.py b/backend/test_deep_audit_regressions.py index cce1610..c6b4c59 100644 --- a/backend/test_deep_audit_regressions.py +++ b/backend/test_deep_audit_regressions.py @@ -665,6 +665,14 @@ def test_homepage_has_low_risk_funnel_analytics_events(self): required_snippets = [ "function trackEvent(eventName, params = {})", "gtag('event', eventName, params)", + "function trackRecommendedEvent(eventName, params = {})", + "function trackConfiguredKeyEvent(eventName, params = {})", + "function trackSpaPageView(path)", + "send_page_view: false", + "page_path: pagePath", + "trackSpaPageView(path)", + "sign_up", + "generate_lead", "function searchHeroServices()", "hero_search_submit", "post_task_cta_click", diff --git a/frontend/index.html b/frontend/index.html index 9254f49..2947e0e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,7 +20,9 @@ window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); - gtag('config', 'G-KM69M3NES8'); + // SPA routes are hash-based, so disable the automatic initial page_view + // and send explicit virtual page_view events from the router instead. + gtag('config', 'G-KM69M3NES8', { send_page_view: false }); @@ -353,6 +355,7 @@

Common agent-delegated work

localStorage.setItem('ghh_user', JSON.stringify(user)); } catch(e) {} toast('Welcome to GoHireHumans!', 'success'); + trackRecommendedEvent('login', { method: 'google' }); navigate('#/'); }) .catch(err => toast(err.message || 'Google sign-in failed', 'error')); @@ -518,6 +521,41 @@

Common agent-delegated work

// Analytics must never block marketplace navigation or checkout intent. } } +function trackRecommendedEvent(eventName, params = {}) { + // GA4 recommended event names make acquisition and conversion reporting work + // without relying only on custom events that must be separately interpreted. + trackEvent(eventName, { + ...params, + recommended_event: true, + marketplace: 'gohirehumans' + }); +} +function trackConfiguredKeyEvent(eventName, params = {}) { + // These event names are already configured as GA4 key events for the + // GoHireHumans property, so emit them for comparable funnel reporting. + trackEvent(eventName, { + ...params, + key_event: true, + marketplace: 'gohirehumans' + }); +} +let lastTrackedSpaPath = ''; +function trackSpaPageView(path) { + const pagePath = location.pathname + location.search + location.hash; + if (lastTrackedSpaPath === pagePath) return; + lastTrackedSpaPath = pagePath; + trackEvent('spa_page_view', { path, page_path: pagePath }); + try { + if (typeof window.gtag === 'function') { + window.gtag('event', 'page_view', { + page_title: document.title, + page_location: location.href, + page_path: pagePath, + spa_route: path + }); + } + } catch (_) {} +} const analyticsSeenForms = new Set(); function analyticsFormName(form) { if (!form) return 'unknown_form'; @@ -549,7 +587,10 @@

Common agent-delegated work

if (!link) return; const href = link.getAttribute('href') || ''; if (href.startsWith('mailto:') || href.startsWith('tel:')) { - trackEvent('contact_click', { link_type: href.startsWith('mailto:') ? 'email' : 'phone', source: location.pathname + location.hash }); + const linkType = href.startsWith('mailto:') ? 'email' : 'phone'; + trackEvent('contact_click', { link_type: linkType, source: location.pathname + location.hash }); + trackRecommendedEvent('generate_lead', { lead_type: 'contact_click', link_type: linkType, source: location.pathname + location.hash }); + trackConfiguredKeyEvent('qualify_lead', { lead_type: 'contact_click', link_type: linkType, source: location.pathname + location.hash }); } else if (/stripe|checkout|buy|payment/i.test(href)) { trackEvent('payment_intent_click', { destination: href, source: location.pathname + location.hash }); } else if (link.hostname && link.hostname !== location.hostname) { @@ -1729,8 +1770,12 @@

${esc(svc.title)}

showModal('Confirm Order', `

Your payment will be processed through the configured checkout flow after you confirm the order.

Workers receive the listed payout. Employers pay Stripe processing plus a 1% GoHireHumans fee.

Payment processing by Stripe where configured
`, async () => { try { trackEvent('service_order_submit', { service_id: String(id) }); + trackRecommendedEvent('generate_lead', { lead_type: 'service_order_submit', service_id: String(id) }); + trackConfiguredKeyEvent('qualify_lead', { lead_type: 'service_order_submit', service_id: String(id) }); const order = await api(`/services/${id}/order`, { method: 'POST', body: {} }); trackEvent('service_order_completed', { service_id: String(id), order_id: String(order.id || order.order_id || '') }); + trackRecommendedEvent('generate_lead', { lead_type: 'service_order_completed', service_id: String(id), order_id: String(order.id || order.order_id || '') }); + trackConfiguredKeyEvent('close_convert_lead', { lead_type: 'service_order_completed', service_id: String(id), order_id: String(order.id || order.order_id || '') }); toast('Order placed! Payment follows the configured workflow.', 'success'); navigate(`#/orders/${order.id || order.order_id}`); } catch(err) { toast(err.message, 'error'); } @@ -2149,6 +2194,11 @@

${esc(job.title)}

localStorage.setItem('ghh_user', JSON.stringify(user)); } catch(e) {} trackEvent(mode === 'login' ? 'login_completed' : 'signup_completed', { source: 'auth_form' }); + if (mode === 'login') { + trackRecommendedEvent('login', { method: 'email' }); + } else { + trackRecommendedEvent('sign_up', { method: 'email', source: 'auth_form' }); + } toast(mode === 'login' ? 'Welcome back!' : 'Account created! Welcome to GoHireHumans.', 'success'); const redirect = encodedRedirect ? decodeURIComponent(encodedRedirect) : ''; navigate(redirect ? `#/${redirect}` : '#/'); @@ -2469,6 +2519,8 @@

${editId ? 'Edit Service' : 'Post a Service'}

} else { const svc = await api('/services', { method: 'POST', body }); trackEvent('service_post_completed', { service_id: String(svc.id || '') }); + trackRecommendedEvent('generate_lead', { lead_type: 'service_post_completed', service_id: String(svc.id || ''), provider_type: body.provider_type || 'human' }); + trackConfiguredKeyEvent('qualify_lead', { lead_type: 'service_post_completed', service_id: String(svc.id || ''), provider_type: body.provider_type || 'human' }); toast('Service posted!', 'success'); navigate(`#/services/${svc.id}`); } @@ -2596,6 +2648,8 @@

Post a Job

trackEvent('job_post_submit', { budget_type: body.budget_type, category: body.category || '' }); const job = await api('/jobs', { method: 'POST', body }); trackEvent('job_post_completed', { job_id: String(job.id || ''), budget_type: body.budget_type, category: body.category || '' }); + trackRecommendedEvent('generate_lead', { lead_type: 'job_post_completed', job_id: String(job.id || ''), budget_type: body.budget_type, category: body.category || '' }); + trackConfiguredKeyEvent('qualify_lead', { lead_type: 'job_post_completed', job_id: String(job.id || ''), budget_type: body.budget_type, category: body.category || '' }); toast('Job posted!', 'success'); navigate(`#/jobs/${job.id}`); } catch(err) { trackEvent('job_post_failed', { error_message: String(err.message || '').slice(0, 120), budget_type: body.budget_type, category: body.category || '' }); toast(err.message, 'error'); } @@ -3849,7 +3903,7 @@

Report Issues

Use the dispute and reporting systems to surface concer function render() { const hash = location.hash.slice(1) || '/'; const path = hash.split('?')[0]; - trackEvent('spa_page_view', { path }); + trackSpaPageView(path); const segments = path.split('/').filter(Boolean); // ── Public routes ────────────────────────────────────────