1/** 2 * WIRE Miami: Tracking & Analytics 3 * ================================== 4 * Requires: wire-config.js loaded BEFORE this file. 5 * 6 * Handles: 7 * - FUB parameter capture, source detection, and lead submission (global scope) 8 * - Google Ads tag loading + conversion tracking 9 * - UTM/GCLID capture from URL â localStorage + cookie 10 * - Phone call click tracking (tel: links) 11 * - WhatsApp click tracking (wa.me links) 12 * - Email click tracking (mailto: links) 13 * - Scroll depth milestones (25%, 50%, 75%, 100%) 14 * 15 * LOAD THIS: at end of <body>, after wire-config.js 16 * <script src="wire-config.js"></script> 17 * <script src="wire-tracking.js" defer></script> 18 */ 19 20/* ââ WIRE: Capture & persist URL parameters for FUB attribution ââ */ 21(function wireParamCapture() { 22 var params = new URLSearchParams(window.location.search); 23 var keys = ['gclid','utm_source','utm_medium','utm_campaign','utm_content','utm_term']; 24 keys.forEach(function(key) { 25 var val = params.get(key); 26 if (val) { 27 try { localStorage.setItem('wire_' + key, val); } catch(e) {} 28 if (key === 'gclid') { 29 document.cookie = 'wire_gclid=' + encodeURIComponent(val) + 30 ';max-age=7776000;path=/;SameSite=Lax'; 31 } 32 } 33 }); 34})(); 35 36function wireGetStoredParam(key) { 37 try { return localStorage.getItem('wire_' + key) || ''; } catch(e) { return ''; } 38} 39 40function wireGetGclid() { 41 var val = wireGetStoredParam('gclid'); 42 if (val) return val; 43 var match = document.cookie.match(/wire_gclid=([^;]+)/); 44 return match ? decodeURIComponent(match[1]) : ''; 45} 46 47function wireDetectSource() { 48 var gclid = wireGetGclid(); 49 var src = wireGetStoredParam('utm_source').toLowerCase(); 50 var med = wireGetStoredParam('utm_medium').toLowerCase(); 51 var ref = (document.referrer || '').toLowerCase(); 52 53 if (gclid || (src === 'google' && (med === 'cpc' || med === 'ppc'))) 54 return 'Google Ads'; 55 if (src === 'facebook' && (med === 'paid' || med === 'cpc' || med === 'paidsocial')) 56 return 'Facebook Ads'; 57 if (med === 'email') return 'Email'; 58 if (src === 'whatsapp') return 'WhatsApp'; 59 if (src === 'instagram' || ref.indexOf('instagram') !== -1) return 'Instagram'; 60 if (ref.indexOf('google') !== -1) return 'Google Organic'; 61 if (ref.indexOf('facebook') !== -1) return 'Facebook Organic'; 62 if (ref.indexOf('bing') !== -1) return 'Bing'; 63 if (ref && ref.indexOf(window.location.hostname) === -1) return 'Referral'; 64 return 'Direct'; 65} 66 67/** 68 * Submit lead to Follow Up Boss via Events API. 69 * Includes 5-second dedup guard to prevent duplicate submissions 70 * when multiple gate elements fire simultaneously. 71 * @param {Object} formData - { firstName, lastName, email, phone (optional) } 72 * @param {string} regType - e.g., 'Floor Plan Request', 'Contact Form', 'General Registration' 73 */ 74/* ââ Meta Pixel + Conversions API (2026-09-24, RUN NAME Meta Pixel And CAPI) ââ 75 One WIRE pixel for every site. The browser event and the server event 76 (Supabase meta-capi) share an eventID so Meta counts each lead once, and 77 still counts it when an ad blocker stops the browser pixel. 78 Per-site opt-out: WIRE.tracking.metaPixelId = false. Override: a pixel id string. */ 79var WIRE_META_PIXEL_DEFAULT = '1727031640809952'; 80function wireMetaPixelId() { 81 var t = (window.WIRE && window.WIRE.tracking) || {}; 82 if (t.metaPixelId === false) return ''; 83 return String(t.metaPixelId || WIRE_META_PIXEL_DEFAULT); 84} 85function wireCookie(name) { 86 var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); 87 return m ? decodeURIComponent(m[1]) : ''; 88} 89function wireMetaFbc() { 90 var c = wireCookie('_fbc'); 91 if (c) return c; 92 var m = location.search.match(/[?&]fbclid=([^&#]+)/); 93 return m ? 'fb.1.' + Date.now() + '.' + decodeURIComponent(m[1]) : ''; 94} 95(function() { 96 var pid = wireMetaPixelId(); 97 if (!pid || window.fbq) return; 98 var n = window.fbq = function() { 99 n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); 100 }; 101 if (!window._fbq) window._fbq = n; 102 n.push = n; n.loaded = true; n.version = '2.0'; n.queue = []; 103 window.fbq('init', pid); 104 window.fbq('track', 'PageView'); 105})(); 106function wireMetaEvent(name, formData, regType) { 107 try { 108 if (!wireMetaPixelId() || typeof window.fbq !== 'function') return; 109 var prop = (window.WIRE && window.WIRE.property) || {}; 110 var id = String(name).toLowerCase() + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); 111 var custom = { content_name: prop.name || '', content_category: regType || '' }; 112 window.fbq('track', name, custom, { eventID: id }); 113 var fd = formData || {}; 114 fetch('https://xlbdalsxqzhkrupqoclm.supabase.co/functions/v1/meta-capi', { 115 method: 'POST', 116 headers: { 'Content-Type': 'application/json' }, 117 keepalive: true, 118 body: JSON.stringify({ 119 event_name: name, event_id: id, event_source_url: location.href, 120 email: fd.email || '', phone: fd.phone || '', 121 first_name: fd.firstName || '', last_name: fd.lastName || '',
122 fbp: wireCookie('_fbp'), fbc: wireMetaFbc(), 123 content_name: custom.content_name, content_category: custom.content_category 124 }) 125 }).catch(function() {}); 126 } catch (e) { console.warn('[WIRE] Meta event error:', e); } 127} 128 129var _wireFubLastSubmit = { email: '', time: 0 }; 130 131function wireSubmitToFUB(formData, regType) { 132 var cfg = window.WIRE; 133 if (!cfg || !cfg.tracking) return; 134 // Either the handler route is enabled (fubViaHandler) or a direct FUB key 135 // must be present. With neither, there is nothing to submit to. 136 if (!cfg.tracking.fubViaHandler && !cfg.tracking.fubApiKey) return; 137 138 // Dedup guard: skip if same email submitted within 5 seconds 139 var now = Date.now(); 140 if (formData.email && formData.email === _wireFubLastSubmit.email && (now - _wireFubLastSubmit.time) < 5000) { 141 console.log('[WIRE] FUB dedup: skipping duplicate submission for', formData.email); 142 return; 143 } 144 _wireFubLastSubmit = { email: formData.email || '', time: now }; 145 // rv-3 (2026-09-29): the Meta Lead event fires only after the lead is 146 // confirmed saved (handler or FUB answered 2xx), never on a failed attempt. 147 var wireLeadSaved = function() { wireMetaEvent('Lead', formData, regType || 'General Registration'); }; 148 149 var propertyTag = 'Property: ' + cfg.property.name; 150 var sourceTag = 'Source: ' + wireDetectSource(); 151 var regTag = 'Reg: ' + (regType || 'General Registration'); 152 var tags = [propertyTag, sourceTag, regTag]; 153 154 // 619 Nobu VIP tag 155 if (cfg.property.domain && cfg.property.domain.indexOf('619brickellav') !== -1) { 156 tags.push('Status: VIP - 619 Nobu'); 157 } 158 159 // Buyer-or-agent classification (FI-1 convention: plain tag + description 160 // prefix; broker normalizes to agent; unclassified gets neither). 161 var ct = String(formData.contactType || '').toLowerCase().trim(); 162 if (ct === 'broker' || ct === 'realtor') ct = 'agent'; 163 if (ct !== 'buyer' && ct !== 'agent') ct = ''; 164 if (ct === 'buyer') tags.push('Buyer'); 165 if (ct === 'agent') tags.push('Agent'); 166 var ctPrefix = ct === 'buyer' ? '[BUYER] ' : ct === 'agent' ? '[AGENT] ' : ''; 167 168 // Qualification summary (2026-06-11): the optional form answers now travel 169 // with the event so FUB shows them in the lead description. 170 var qualBits = []; 171 if (formData.income) qualBits.push('Income: ' + formData.income); 172 if (formData.timeline) qualBits.push('Timeline: ' + formData.timeline); 173 if (formData.bedrooms) qualBits.push('Bedrooms: ' + formData.bedrooms); 174 if (formData.pricePoint) qualBits.push('Price point: ' + formData.pricePoint); 175 var qualSummary = qualBits.length ? ' | ' + qualBits.join(' | ') : ''; 176 177 // Build custom fields (omit empty values) 178 var cf = {}; 179 var gclid = wireGetGclid(); 180 if (gclid) cf.customGCLID = gclid; 181 cf.customPrimaryPropertyInterest = cfg.property.name; 182 var utmFields = { 183 customUTMSource: wireGetStoredParam('utm_source'), 184 customUTMMedium: wireGetStoredParam('utm_medium'), 185 customUTMCampaign: wireGetStoredParam('utm_campaign'), 186 customUTMContent: wireGetStoredParam('utm_content'), 187 customUTMTerm: wireGetStoredParam('utm_term') 188 }; 189 Object.keys(utmFields).forEach(function(k) { 190 if (utmFields[k]) cf[k] = utmFields[k]; 191 }); 192 if (ct) cf.customContactType = ct; // read by the handler for the CRM lead_type 193 // Launch Night Sites 2026-09-26 (from the hub kit): which page the lead came 194 // from. FUB custom field + description; the handler also takes body.pageUrl. 195 var wirePageUrl = ''; 196 try { wirePageUrl = location.origin + location.pathname; } catch (e) {} 197 if (wirePageUrl) cf.customSourceURL = wirePageUrl; 198 199 var person = { 200 firstName: formData.firstName || '', 201 lastName: formData.lastName || '', 202 emails: [{ value: formData.email || '' }], 203 tags: tags 204 }; 205 // FUB shape fix 2026-08-27: the events API persists custom fields ONLY as 206 // top-level person keys; a nested customFields object is silently dropped 207 // (verified live against the account). Spread cf onto the person. 208 Object.keys(cf).forEach(function(k) { person[k] = cf[k]; }); 209 if (formData.phone) { 210 person.phones = [{ value: formData.phone }]; 211 } 212 213 var payload = { 214 source: cfg.property.shortName || cfg.property.name, 215 system: cfg.tracking.fubSystem || 'WIREMiami', 216 type: 'Registration', 217 person: person, 218 description: (ctPrefix + regType + ': ' + cfg.property.name + ' via ' + cfg.property.domain + 219 (wirePageUrl ? ' | Page: ' + location.pathname : '') + qualSummary).slice(0, 300) 220 }; 221 if (formData.message) payload.message = String(formData.message).slice(0, 1000); 222 223 // 619 pilot: when fubViaHandler is set, POST the IDENTICAL event payload to 224 // the WIRE Concierge handler instead of FUB directly. The server holds the 225 // FUB key (env), creates the same /v1/events lead, and applies the project 226 // action plan. Sites without this flag are unaffected. 227 if (cfg.tracking.fubViaHandler) {
228 fetch('https://wire-concierge-production.up.railway.app/api/form-lead', { 229 method: 'POST', 230 headers: { 'Content-Type': 'application/json' }, 231 body: JSON.stringify({ 232 event: payload, 233 propertyDomain: cfg.property.domain || '', 234 // 2026-09-02: display name for sites with no project JSON (the hub); 235 // the handler prefers the project JSON name when one resolves. 236 propertyName: cfg.property.name || '', 237 // 2026-09-14 (Fleet Known Bugs Sweep, queue 97): page facts, so the CRM can 238 // tie an LLM referral to the page the assistant cited. The handler reads 239 // body.pageUrl and body.referrer; the Referer header carries no path under 240 // the kit's strict-origin-when-cross-origin policy, so the body must. 241 pageUrl: (document.location && document.location.href) || '', 242 referrer: document.referrer || '', 243 regType: regType || 'General Registration', 244 // Option B: tell the handler this kit no longer writes wire-crm from the 245 // browser, so the handler owns the dual-write. Removing the wireSubmitToWIRE 246 // calls (wire-contact-form.js / wire-lead-capture.js) is the matching half. 247 wireCrmHandledByServer: true, 248 // RT-4 (Launch Night Sites 2026-09-26): version of the consent disclosure 249 // shown under every form and modal on this site (619 wording, 2026-09-26). 250 consentDisclosure: (cfg.tracking && cfg.tracking.consentDisclosure) || 'wire-619-v1', 251 // 2026-06-11: optional qualification answers, passed for the handler's 252 // CRM dual-write (same field names the legacy browser wireSubmitToWIRE 253 // contract used with lead-intake). 254 qualification: { 255 contactType: ct, 256 income: formData.income || '', 257 timeline: formData.timeline || '', 258 bedrooms: formData.bedrooms || '', 259 pricePoint: formData.pricePoint || '', 260 message: formData.message || '', 261 buyerType: formData.buyerType || '' 262 } 263 }) 264 }) 265 .then(function(r) { 266 if (!r.ok) console.warn('[WIRE] handler form-lead failed:', r.status); 267 else { console.log('[WIRE] Lead sent via handler:', formData.email); wireLeadSaved(); } 268 }) 269 .catch(function(e) { 270 console.warn('[WIRE] handler form-lead error:', e); 271 }); 272 return; 273 } 274 275 var apiKey = cfg.tracking.fubApiKey; 276 277 fetch('https://api.followupboss.com/v1/events', { 278 method: 'POST', 279 headers: { 280 'Content-Type': 'application/json', 281 'Authorization': 'Basic ' + btoa(apiKey + ':') 282 }, 283 body: JSON.stringify(payload) 284 }) 285 .then(function(r) { 286 if (!r.ok) console.warn('[WIRE] FUB submission failed:', r.status); 287 else { console.log('[WIRE] Lead sent to FUB:', formData.email); wireLeadSaved(); } 288 }) 289 .catch(function(e) { 290 console.warn('[WIRE] FUB error:', e); 291 }); 292} 293 294/** 295 * Submit lead to the WIRE CRM intake endpoint, in parallel with FUB. 296 * Public endpoint, no key required. Fire-and-forget; never blocks the FUB call. 297 * Reads the SAME formData object that wireSubmitToFUB receives. 298 */ 299function wireSubmitToWIRE(formData, regType) { 300 try { 301 var cfg = window.WIRE; 302 if (!formData || (!formData.email && !formData.phone)) return; 303 304 var prop = (cfg && cfg.property) ? cfg.property : {}; 305 var payload = { 306 firstName: formData.firstName || '', 307 lastName: formData.lastName || '', 308 email: formData.email || '', 309 phone: formData.phone || '', 310 property: prop.name || '', 311 domain: prop.domain || (location && location.hostname) || '', 312 regType: regType || 'General Registration', 313 source: (typeof wireDetectSource === 'function') ? wireDetectSource() : '', 314 gclid: (typeof wireGetGclid === 'function') ? wireGetGclid() : '', 315 utm: { 316 source: (typeof wireGetStoredParam === 'function') ? wireGetStoredParam('utm_source') : '', 317 medium: (typeof wireGetStoredParam === 'function') ? wireGetStoredParam('utm_medium') : '',
318 campaign: (typeof wireGetStoredParam === 'function') ? wireGetStoredParam('utm_campaign') : '', 319 content: (typeof wireGetStoredParam === 'function') ? wireGetStoredParam('utm_content') : '', 320 term: (typeof wireGetStoredParam === 'function') ? wireGetStoredParam('utm_term') : '' 321 }, 322 message: formData.message || '', 323 buyerType: formData.buyerType || '', 324 income: formData.income || '', 325 timeline: formData.timeline || '', 326 pricePoint: formData.pricePoint || '', 327 bedrooms: formData.bedrooms || '' 328 }; 329 330 fetch('https://xlbdalsxqzhkrupqoclm.supabase.co/functions/v1/lead-intake', { 331 method: 'POST', 332 headers: { 'Content-Type': 'application/json' }, 333 body: JSON.stringify(payload) 334 }) 335 .then(function(r) { 336 if (!r.ok) console.warn('[WIRE] CRM intake failed:', r.status); 337 else console.log('[WIRE] Lead sent to CRM:', formData.email || formData.phone); 338 }) 339 .catch(function(e) { console.warn('[WIRE] CRM intake error:', e); }); 340 } catch (e) { console.warn('[WIRE] wireSubmitToWIRE error:', e); } 341} 342 343 344/* ââ End FUB helpers (global scope) ââ */ 345 346(function() { 347 'use strict'; 348 349 var W = window.WIRE; 350 if (!W) { console.error('wire-config.js must load before wire-tracking.js'); return; } 351 352 /* ââââââââââââââââââââââââââââââ 353 META PIXEL LOADER (queue is set up at parse time above; the library 354 loads after the page, like gtag, so it never blocks rendering) 355 ââââââââââââââââââââââââââââââ */ 356 357 window.addEventListener('load', function() { 358 if (!wireMetaPixelId() || document.getElementById('wire-fbevents')) return; 359 var f = document.createElement('script'); 360 f.id = 'wire-fbevents'; 361 f.async = true; 362 f.src = 'https://connect.facebook.net/en_US/fbevents.js'; 363 document.head.appendChild(f); 364 }); 365 366 /* ââââââââââââââââââââââââââââââ 367 GOOGLE ADS / GTAG SETUP 368 ââââââââââââââââââââââââââââââ */ 369 370 // Load gtag.js asynchronously on window load (non-render-blocking) 371 window.addEventListener('load', function() { 372 // RT-39 (2026-09-26): the page head already loads gtag.js and runs config; 373 // skip so gtag.js is not injected twice and config does not fire twice. 374 if (document.querySelector('script[src*="googletagmanager.com/gtag/js"]')) return; 375 if (!W.tracking.googleAdsId) return; 376 377 var s = document.createElement('script'); 378 s.src = 'https://www.googletagmanager.com/gtag/js?id=' + W.tracking.googleAdsId; 379 s.async = true; 380 document.head.appendChild(s); 381 382 s.onload = function() { 383 window.dataLayer = window.dataLayer || []; 384 function gtag() { dataLayer.push(arguments); } 385 window.gtag = gtag; 386 gtag('js', new Date()); 387 gtag('config', W.tracking.googleAdsId); 388 389 // Also config GA4 if provided 390 if (W.tracking.ga4Id) { 391 gtag('config', W.tracking.ga4Id); 392 } 393 }; 394 }); 395 396 /* UTM + GCLID capture is now handled by wireParamCapture() in global scope above */ 397 398 /* ââââââââââââââââââââââââââââââ 399 PHONE CALL CLICK TRACKING 400 ââââââââââââââââââââââââââââââ */ 401 402 document.addEventListener('click', function(e) { 403 var link = e.target.closest('a[href^="tel:"]'); 404 if (!link) return; 405 wireMetaEvent('Contact', null, 'Phone Call'); 406 407 if (typeof gtag === 'function') { 408 // Launch Night Sites 2026-09-26 (from the hub kit): tap clicks no longer fire 409 // the FORM lead conversion (Maximize Conversions would bid on taps). They fire 410 // only a dedicated action when tracking.googleAdsPhoneConversion is set. 411 if (W.tracking.googleAdsPhoneConversion) { 412 gtag('event', 'conversion', { 413 'send_to': W.tracking.googleAdsPhoneConversion, 414 'value': 0.5, 415 'currency': 'USD' 416 }); 417 } 418 gtag('event', 'phone_call_click', { 419 'event_category': 'engagement', 420 'event_label': link.href 421 }); 422 } 423 }); 424 425 /* ââââââââââââââââââââââââââââââ 426 WHATSAPP CLICK TRACKING 427 ââââââââââââââââââââââââââââââ */ 428 429 document.addEventListener('click', function(e) { 430 var link = e.target.closest('a[href*="wa.me"]'); 431 if (!link) return; 432 wireMetaEvent('Contact', null, 'WhatsApp'); 433 434 if (typeof gtag === 'function') { 435 // Launch Night Sites 2026-09-26 (from the hub kit): tap clicks no longer fire 436 // the FORM lead conversion (Maximize Conversions would bid on taps). They fire 437 // only a dedicated action when tracking.googleAdsWhatsAppConversion is set. 438 if (W.tracking.googleAdsWhatsAppConversion) { 439 gtag('event', 'conversion', { 440 'send_to': W.tracking.googleAdsWhatsAppConversion, 441 'value': 0.5, 442 'currency': 'USD' 443 }); 444 } 445 gtag('event', 'whatsapp_click', { 446 'event_category': 'engagement', 447 'event_label': W.property.name 448 }); 449 } 450 }); 451 452 /* ââââââââââââââââââââââââââââââ 453 EMAIL CLICK TRACKING 454 ââââââââââââââââââââââââââââââ */ 455 456 document.addEventListener('click', function(e) { 457 var link = e.target.closest('a[href^="mailto:"]'); 458 if (!link) return; 459 460 if (typeof gtag === 'function') { 461 gtag('event', 'email_click', { 462 'event_category': 'engagement', 463 'event_label': link.href 464 }); 465 } 466 }); 467 468 /* ââââââââââââââââââââââââââââââ 469 SCROLL DEPTH TRACKING 470 ââââââââââââââââââââââââââââââ */ 471 472 (function() { 473 var milestones = { 25: false, 50: false, 75: false, 100: false }; 474 475 window.addEventListener('scroll', function() { 476 var scrollPct = Math.round(
477 (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100 478 ); 479 480 [25, 50, 75, 100].forEach(function(milestone) { 481 if (scrollPct >= milestone && !milestones[milestone]) { 482 milestones[milestone] = true; 483 if (typeof gtag === 'function') { 484 gtag('event', 'scroll_depth', { 485 'event_category': 'engagement', 486 'event_label': milestone + '%', 487 'value': milestone 488 }); 489 } 490 } 491 }); 492 }, { passive: true }); 493 })(); 494 495 /* ââââââââââââââââââââââââââââââ 496 TIME ON SITE TRACKING 497 ââââââââââââââââââââââââââââââ */ 498 499 (function() { 500 var thresholds = [30, 60, 120, 300]; // seconds 501 var fired = {}; 502 503 setInterval(function() { 504 var elapsed = Math.round((Date.now() - performance.timing.navigationStart) / 1000); 505 thresholds.forEach(function(t) { 506 if (elapsed >= t && !fired[t]) { 507 fired[t] = true; 508 if (typeof gtag === 'function') { 509 gtag('event', 'time_on_site', { 510 'event_category': 'engagement', 511 'event_label': t + 's', 512 'value': t 513 }); 514 } 515 } 516 }); 517 }, 5000); 518 })(); 519 520})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.