PageSourceSearch

https://stregisluxuryresidencesmiami.com/wire-tracking.js

js stregisluxuryresidencesmiami.com collected 2026-09-29 07:09:38 UTC 21,770 bytes, 520 lines download raw bytes

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.