/** * 1jia.tw 統一分析追蹤 * GA4 + Meta Pixel + 自動轉換事件追蹤 * * 事件清單(GA4 / Meta Pixel): * - page_view / PageView (自動) * - generate_lead / Lead (表單送出) * - phone_click / Contact (tel: 點擊、撥號按鈕) * - line_click / Contact (LINE 連結點擊) * - social_click / — (FB / IG 連結點擊) * - cta_click / — (重點 CTA 按鈕,需加 data-track-cta="名稱") */ (function () { 'use strict'; // ========================= // 設定區(修改 ID 請在此處) // ========================= const GA4_ID = 'G-FQ4RGZPQ7T'; const META_PIXEL_ID = '1966324590644589'; // ========================= // GA4 初始化 // ========================= if (GA4_ID) { const ga = document.createElement('script'); ga.async = true; ga.src = 'https://www.googletagmanager.com/gtag/js?id=' + GA4_ID; document.head.appendChild(ga); window.dataLayer = window.dataLayer || []; window.gtag = function () { window.dataLayer.push(arguments); }; gtag('js', new Date()); gtag('config', GA4_ID, { page_title: document.title, page_location: window.location.href, page_path: window.location.pathname, anonymize_ip: true }); } // ========================= // Meta Pixel 初始化(填 ID 後自動啟用) // ========================= if (META_PIXEL_ID) { !function (f, b, e, v, n, t, s) { if (f.fbq) return; n = f.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments) }; if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s) }(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', META_PIXEL_ID); fbq('track', 'PageView'); } // ========================= // 統一追蹤 API(供外部 JS 手動呼叫) // window.track('event_name', { foo: 'bar' }) // ========================= window.track = function (name, params) { params = params || {}; try { if (window.gtag) gtag('event', name, params); } catch (e) {} try { if (window.fbq) fbq('trackCustom', name, params); } catch (e) {} }; // ========================= // 轉換漏斗 (Funnel) 追蹤 // - GA4 全寫 // - Firestore 只寫關鍵轉換事件(從 submit_* 開始) // - 自動帶 session_id / UTM / device / referrer // 詳見 docs/funnel-events.md // ========================= const FUNNEL_ENDPOINT = '/.netlify/functions/funnel-track'; // 寫入 Firestore 的事件清單(其餘只走 GA4) const FUNNEL_PERSIST_EVENTS = new Set([ 'submit_inquiry', 'consult_contacted', 'view_scheduled', 'view_done', 'contract_signed', 'submit_landlord', 'landlord_contacted', 'landlord_signed' ]); // ---- session_id(每個 tab 一個)---- function getSessionId() { try { let sid = sessionStorage.getItem('1jia_sid'); if (!sid) { sid = 's_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); sessionStorage.setItem('1jia_sid', sid); } return sid; } catch (e) { return 's_anon'; } } // ---- UTM 捕獲(first 永久;last 每 session)---- function captureUtm() { try { const url = new URL(location.href); const src = url.searchParams.get('utm_source'); const med = url.searchParams.get('utm_medium'); const cmp = url.searchParams.get('utm_campaign'); // referrer 自動推導(無 utm 但有外部 referrer 時) let inferredSrc = src; let inferredMed = med; if (!src && document.referrer) { try { const r = new URL(document.referrer); if (r.hostname && r.hostname !== location.hostname) { inferredSrc = r.hostname.replace(/^www\./, ''); inferredMed = 'referral'; } } catch (e) {} } const utm = { source: inferredSrc || '(direct)', medium: inferredMed || '(none)', campaign: cmp || '' }; // last_utm — 只要有外部來源或帶 utm 就更新 if (src || (document.referrer && new URL(document.referrer).hostname !== location.hostname)) { sessionStorage.setItem('1jia_last_utm', JSON.stringify(utm)); } // first_utm — 永遠不覆寫 if (!localStorage.getItem('1jia_first_utm')) { localStorage.setItem('1jia_first_utm', JSON.stringify(utm)); } } catch (e) {} } function readUtm(key, storage) { try { const raw = storage.getItem(key); if (!raw) return { source: '(direct)', medium: '(none)', campaign: '' }; return JSON.parse(raw); } catch (e) { return { source: '(direct)', medium: '(none)', campaign: '' }; } } function detectDevice() { const ua = navigator.userAgent || ''; if (/iPad|tablet/i.test(ua)) return 'tablet'; if (/Mobi|Android|iPhone/i.test(ua)) return 'mobile'; return 'desktop'; } function buildEnvelope(eventName, params) { const firstUtm = readUtm('1jia_first_utm', localStorage); const lastUtm = readUtm('1jia_last_utm', sessionStorage); return { event: eventName, client_ts: Date.now(), session_id: getSessionId(), page_path: location.pathname, page_url: location.href, referrer: document.referrer || '', first_utm: firstUtm, last_utm: lastUtm, device_type: detectDevice(), params: params || {} }; } // 主要對外 API // window.trackFunnel('submit_inquiry', { house_id, service }) window.trackFunnel = function (eventName, params) { if (!eventName) return; const envelope = buildEnvelope(eventName, params || {}); // GA4:把 envelope 攤平成一層(GA4 不支援巢狀) const ga4Params = { session_id: envelope.session_id, page_path: envelope.page_path, device_type: envelope.device_type, first_utm_source: envelope.first_utm.source, first_utm_medium: envelope.first_utm.medium, first_utm_campaign: envelope.first_utm.campaign, last_utm_source: envelope.last_utm.source, last_utm_medium: envelope.last_utm.medium, last_utm_campaign: envelope.last_utm.campaign, ...(params || {}) }; try { if (window.gtag) gtag('event', eventName, ga4Params); } catch (e) {} try { if (window.fbq) fbq('trackCustom', eventName, ga4Params); } catch (e) {} // Firestore:只寫關鍵轉換事件 if (FUNNEL_PERSIST_EVENTS.has(eventName)) { try { const payload = JSON.stringify(envelope); // 優先用 sendBeacon(即使頁面卸載也能送出) if (navigator.sendBeacon) { const blob = new Blob([payload], { type: 'application/json' }); navigator.sendBeacon(FUNNEL_ENDPOINT, blob); } else { fetch(FUNNEL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true }).catch(function () {}); } } catch (e) {} } }; // 頁面載入時就先抓一次 UTM captureUtm(); // ========================= // 自動轉換事件綁定 // ========================= function bindAutoTracking() { // 1. 表單送出(房東委託、諮詢表單皆自動偵測) document.querySelectorAll('form').forEach(function (form) { form.addEventListener('submit', function () { const formId = form.id || form.getAttribute('name') || 'unknown_form'; const identity = (form.querySelector('[name="identity"]') || {}).value || ''; const service = (form.querySelector('[name="service"]') || {}).value || ''; if (window.gtag) { gtag('event', 'generate_lead', { form_id: formId, identity: identity, service: service, page_path: location.pathname }); } if (window.fbq) fbq('track', 'Lead', { content_name: formId }); }); }); // 2. 撥號(tel: 與 #float-phone) document.querySelectorAll('a[href^="tel:"], #float-phone').forEach(function (el) { el.addEventListener('click', function () { const href = el.getAttribute('href') || ''; const phone = href.replace('tel:', ''); if (window.gtag) { gtag('event', 'phone_click', { phone_number: phone || 'float_btn', page_path: location.pathname }); } if (window.fbq) fbq('track', 'Contact', { method: 'phone' }); }); }); // 3. LINE 點擊(浮動按鈕、footer、任何指向 line.me / lin.ee 的連結) document.querySelectorAll('#float-line, #link-line, a[href*="line.me"], a[href*="lin.ee"]').forEach(function (el) { el.addEventListener('click', function () { if (window.gtag) { gtag('event', 'line_click', { page_path: location.pathname, link_location: el.id || 'inline' }); } if (window.fbq) fbq('track', 'Contact', { method: 'line' }); }); }); // 4. FB / IG 點擊 document.querySelectorAll('#float-fb, #link-fb, #float-ig, #link-ig').forEach(function (el) { el.addEventListener('click', function () { const platform = el.id.indexOf('fb') >= 0 ? 'facebook' : 'instagram'; if (window.gtag) { gtag('event', 'social_click', { platform: platform, page_path: location.pathname }); } }); }); // 5. 重點 CTA(替按鈕加 data-track-cta="名稱" 即自動追蹤) document.querySelectorAll('[data-track-cta]').forEach(function (el) { el.addEventListener('click', function () { const ctaName = el.getAttribute('data-track-cta'); if (window.gtag) { gtag('event', 'cta_click', { cta_name: ctaName, page_path: location.pathname }); } if (window.fbq) fbq('trackCustom', 'CTAClick', { cta_name: ctaName }); }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bindAutoTracking); } else { bindAutoTracking(); } })();