PageSourceSearch

https://ohdidi.vn/frontend/home/js/pages/analytics-tracker.js?v=1790672179

js ohdidi.vn collected 2026-10-01 23:50:21 UTC 19,420 bytes, 548 lines download raw bytes

1(function (window, document) {
2  "use strict";
3
4  var endpoint = "/analytics/track";
5  var storagePrefix = "ohdidi_analytics_";
6  var eventQueue = [];
7  var batchTimer = null;
8  var batchInterval = 3000;
9  var maxQueueSize = 10;
10  var isSubmittingSearch = false;
11  var sentNoResults = {};
12
13  var criticalEvents = {
14    booking_confirmed: true,
15    payment_success: true,
16    payment_failed: true,
17    lead_submitted: true,
18    guest_info_submitted: true
19  };
20
21  function uuid() {
22    if (window.crypto && window.crypto.randomUUID) {
23      return window.crypto.randomUUID();
24    }
25    return "id-" + Date.now() + "-" + Math.random().toString(16).slice(2);
26  }
27
28  function getStored(storage, key) {
29    try {
30      var value = storage.getItem(storagePrefix + key);
31      if (!value) {
32        value = uuid();
33        storage.setItem(storagePrefix + key, value);
34      }
35      return value;
36    } catch (e) {
37      return uuid();
38    }
39  }
40
41  function getDeviceType() {
42    var width = window.innerWidth || document.documentElement.clientWidth || 0;
43    if (width <= 767) return "mobile";
44    if (width <= 1024) return "tablet";
45    return "desktop";
46  }
47
48  function cleanText(val) {
49    return String(val == null ? "" : val).replace(/\s+/g, " ").trim();
50  }
51
52  function getAttribution() {
53    var params = new URLSearchParams(window.location.search || "");
54    var source = params.get("utm_source") || params.get("source") || "";
55    var medium = params.get("utm_medium") || "";
56    var campaign = params.get("utm_campaign") || "";
57    var gclid = params.get("gclid") || params.get("gbraid") || params.get("wbraid");
58
59    if (!source) {
60      if (gclid) {
61        source = "google";
62        medium = medium || "cpc";
63      } else if (document.referrer) {
64        try {
65          var refHost = new URL(document.referrer).hostname.replace(/^www\./, "");
66          if (refHost && refHost !== window.location.hostname.replace(/^www\./, "")) {
67            source = refHost.slice(0, 120);
68            medium = medium || "referral";
69          }
70        } catch (e) {}
71      }
72    }
73
74    return {
75      source: cleanText(source).toLowerCase() || "direct",
76      medium: cleanText(medium).toLowerCase() || (source ? "referral" : "none"),
77      campaign: cleanText(campaign)
78    };
79  }
80
81  function pageType() {
82    var path = window.location.pathname;
83    if (path.indexOf("/blog") === 0) return "blog";
84    if (path.indexOf("/tim-kiem") === 0) return "search";
85    if (document.body.classList.contains("home-detail") || document.querySelector("[data-analytics-property-detail]")) return "listing_detail";
86    if (document.querySelector(".box-form__booking") || document.querySelector(".payment__page")) return "booking";
87    if (document.querySelector(".home-item")) return "listing";
88    return "page";
89  }
90
91  function sendGa4(eventName, params) {
92    if (typeof window.gtag === "function") {
93      try {
94        window.gtag("event", eventName, params);
95      } catch (e) {}
96    }
97  }
98
99  // Tự động đồng bộ các sự kiện hành vi chuẩn sang Google Analytics 4 (Dual-Tracking Best Practice)
100  function syncToGa4(eventName, payload) {
101    if (typeof window.gtag !== "function") return;
102    try {
103      payload = payload || {};
104      switch (eventName) {
105        case "search_submitted":
106          sendGa4("search", {
107            search_term: payload.search_keyword || payload.province_code || "tat_ca"
108          });
109          break;
110        case "search_results_viewed":
111          sendGa4("view_search_results", {
112            search_term: payload.search_keyword || payload.province_code || "tat_ca"
113          });
114          break;
115        case "listing_detail_viewed":
116          sendGa4("view_item", {
117            currency: "VND",
118            value: payload.price || undefined,
119            items: [{
120              item_id: String(payload.property_id || ""),
121              item_name: (payload.metadata && payload.metadata.title) || "",
122              item_category: (payload.metadata && payload.metadata.rental_type) || "",
123              item_location_id: payload.province_code || undefined,
124              price: payload.price || undefined,
125              quantity: 1
126            }]
127          });
128          break;
129        case "listing_clicked":
130          sendGa4("select_item", {
131            items: [{
132              item_id: String(payload.property_id || ""),
133              item_name: (payload.metadata && payload.metadata.title) || "",
134              item_category: (payload.metadata && payload.metadata.rental_type) || "",
135              item_location_id: payload.province_code || undefined,
136              price: payload.price || undefined,
137              quantity: 1
138            }]
139          });
140          break;
141        case "date_selected":
142          sendGa4("select_content", {
143            content_type: "date_selection",
144            item_id: "check_available_dates"
145          });
146          break;
147        case "booking_started":
148          sendGa4("begin_checkout", {
149            currency: "VND",
150            value: payload.price || undefined,
151            items: payload.property_id ? [{
152              item_id: String(payload.property_id),
153              quantity: 1
154            }] : []
155          });
156          break;
157        case "guest_info_submitted":
158          sendGa4("add_shipping_info", {
159            currency: "VND",
160            value: payload.price || undefined
161          });
162          break;
163        case "payment_started":
164          sendGa4("add_payment_info", {
165            currency: "VND",
166            payment_type: payload.payment_method || "online"
167          });
168          break;
169        case "booking_confirmed":
170          sendGa4("purchase", {
171            transaction_id: String(payload.booking_id || ("BK-" + Date.now())),
172            value: parseFloat(payload.value || payload.price || 0),
173            currency: "VND",
174            items: payload.property_id ? [{
175              item_id: String(payload.property_id),
176              quantity: 1
177            }] : []
178          });
179          break;
180        case "cta_clicked":
181          var mKey = String(payload.metric_key || "").toLowerCase();
182          if (mKey.indexOf("hotline") !== -1 || mKey.indexOf("phone") !== -1) {
183            sendGa4("generate_lead", { lead_source: "hotline", cta: mKey });
184          } else if (mKey.indexOf("zalo") !== -1) {
185            sendGa4("generate_lead", { lead_source: "zalo", cta: mKey });
186          } else {
187            sendGa4("select_content", { content_type: "cta", item_id: mKey });
188          }
189          break;
190        default:
191          break;
192      }
193    } catch (e) {}
194  }
195
196  function sendTikTok(eventName, params) {
197    if (window.ttq && typeof window.ttq.track === "function") {
198      try {
199        window.ttq.track(eventName, params);
200      } catch (e) {}
201    }
202  }
203
204  // Tự động đồng bộ các sự kiện chuẩn sang TikTok Pixel (Chuẩn Video Shopping Ads - VSA & Catalog)
205  function syncToTikTok(eventName, payload) {
206    if (!window.ttq || typeof window.ttq.track !== "function") return;
207    try {
208      payload = payload || {};
209      var propId = payload.property_id ? String(payload.property_id) : "";
210      var price = payload.price ? parseFloat(payload.price) : 0;
211      var title = (payload.metadata && payload.metadata.title) || "";
212      var category = (payload.metadata && payload.metadata.rental_type) || "homestay";
213
214      switch (eventName) {
215        case "listing_detail_viewed":
216          if (propId) {
217            sendTikTok("ViewContent", {
218              content_id: propId,
219              content_ids: [propId],
220              content_type: "product",
221              content_name: title,
222              content_category: category,
223              price: price || undefined,
224              value: price || undefined,
225              currency: "VND",
226              contents: [{
227                content_id: propId,
228                content_type: "product",
229                content_name: title,
230                price: price || undefined,
231                quantity: 1
232              }]
233            });
234          }
235          break;
236
237        case "booking_started":
238          if (propId) {
239            sendTikTok("InitiateCheckout", {
240              content_id: propId,
241              content_ids: [propId],
242              content_type: "product",
243              content_name: title,
244              price: price || undefined,
245              value: price || undefined,
246              currency: "VND",
247              quantity: 1,
248              contents: [{
249                content_id: propId,
250                content_type: "product",
251                content_name: title,
252                price: price || undefined,
253                quantity: 1
254              }]
255            });
256          }
257          break;
258
259        case "booking_confirmed":
260          var finalVal = parseFloat(payload.value || payload.price || 0);
261          sendTikTok("CompletePayment", {
262            content_id: propId || undefined,
263            content_ids: propId ? [propId] : [],
264            content_type: "product",
265            value: finalVal || undefined,
266            currency: "VND",
267            quantity: 1,
268            contents: propId ? [{
269              content_id: propId,
270              content_type: "product",
271              price: finalVal || undefined,
272              quantity: 1
273            }] : []
274          });
275          break;
276
277        case "search_submitted":
278        case "search_results_viewed":
279          var searchKw = payload.search_keyword || payload.province_code || "tat_ca";
280          sendTikTok("Search", {
281            content_id: searchKw,
282            content_ids: [searchKw],
283            content_type: "product_group",
284            query: searchKw,
285            search_string: searchKw,
286            contents: [{
287              content_id: searchKw,
288              content_type: "product_group",
289              content_name: searchKw
290            }]
291          });
292          break;
293
294        case "cta_clicked":
295          var mKey = String(payload.metric_key || "").toLowerCase();
296          if (mKey.indexOf("hotline") !== -1 || mKey.indexOf("phone") !== -1 || mKey.indexOf("zalo") !== -1) {
297            sendTikTok("Contact", {
298              content_id: propId || "hotline_zalo",
299              content_ids: [propId || "hotline_zalo"],
300              content_name: mKey
301            });
302          }
303          break;
304
305        default:
306          break;
307      }
308    } catch (e) {}
309  }
310
311  function basePayload(extra) {
312    var attr = getAttribution();
313    return Object.assign({
314      session_id: getStored(window.sessionStorage, "session_id"),
315      anonymous_id: getStored(window.localStorage, "anonymous_id"),
316      user_id: window.currentUserId || null,
317      platform: "web",
318      device_type: getDeviceType(),
319      source: attr.source,
320      medium: attr.medium,
321      campaign: attr.campaign,
322      page_type: pageType(),
323      page_url: window.location.pathname + window.location.search
324    }, extra || {});
325  }
326
327  function post(payload, useBeacon) {
328    var body = JSON.stringify(payload);
329    if (navigator.sendBeacon && (useBeacon || body.length < 64000)) {
330      var blob = new Blob([body], { type: "application/json" });
331      if (navigator.sendBeacon(endpoint, blob)) return;
332    }
333    fetch(endpoint, {
334      method: "POST",
335      headers: { "Content-Type": "application/json" },
336      body: body,
337      credentials: "same-origin",
338      keepalive: true
339    }).catch(function () {});
340  }
341
342  function flushQueue(useBeacon) {
343    if (batchTimer) {
344      clearTimeout(batchTimer);
345      batchTimer = null;
346    }
347    if (!eventQueue.length) return;
348    var events = eventQueue.splice(0, eventQueue.length);
349    post({ events: events }, !!useBeacon);
350  }
351
352  function scheduleFlush() {
353    if (batchTimer) return;
354    batchTimer = setTimeout(function () {
355      flushQueue(false);
356    }, batchInterval);
357  }
358
359  function track(eventName, payload) {
360    eventQueue.push(basePayload(Object.assign({ event_name: eventName }, payload || {})));
361    
362    // Đồng bộ tức thời sang GA4 theo chuẩn E-Commerce
363    syncToGa4(eventName, payload);
364
365    // Đồng bộ tức thời sang TikTok Pixel (chuẩn VSA & Catalog)
366    syncToTikTok(eventName, payload);
367
368    if (criticalEvents[eventName] || eventQueue.length >= maxQueueSize) {
369      flushQueue(false);
370      return;
371    }
372    scheduleFlush();
373  }
374
375  function propertyPayload(el) {
376    var rawPrice = el.getAttribute("data-analytics-price");
377    var price = rawPrice ? parseFloat(rawPrice) : null;
378    return {
379      property_id: parseInt(el.getAttribute("data-analytics-property-id") || "0", 10) || null,
380      province_code: el.getAttribute("data-analytics-province") || null,
381      price: price && !isNaN(price) ? price : null,
382      metadata: {
383        title: el.getAttribute("data-analytics-title") || "",
384        rental_type: el.getAttribute("data-analytics-property-type") || ""
385      }
386    };
387  }
388
389  function setupSearchSubmit() {
390    document.addEventListener("submit", function (event) {
391      var form = event.target;
392      if (!form || !form.querySelector || isSubmittingSearch) return;
393
394      var kwInput = form.querySelector('input[name="keyword"], input[name="location"], #keyword, .js-search-keyword');
395      var keyword = kwInput ? cleanText(kwInput.value) : "";
396      var provInput = form.querySelector('input[name="province"], select[name="province"]');
397      var province = provInput ? cleanText(provInput.value) : "";
398
399      isSubmittingSearch = true;
400      track("search_submitted", {
401        search_keyword: keyword,
402        province_code: province,
403        metadata: { form_id: form.id || form.className || "" }
404      });
405      sendGa4("search", {
406        search_term: keyword || province || "tat_ca",
407        search_location: province || undefined
408      });
409      setTimeout(function () { isSubmittingSearch = false; }, 1500);
410    }, true);
411  }
412
413  function setupClicks() {
414    document.addEventListener("click", function (event) {
415      var listing = event.target.closest("[data-analytics-property-id]");
416      if (listing && event.target.closest("a")) {
417        var clickedProp = propertyPayload(listing);
418        track("listing_clicked", clickedProp);
419        sendGa4("select_item", {
420          item_list_name: pageType(),
421          items: [{
422            item_id: String(clickedProp.property_id || ""),
423            item_name: clickedProp.metadata.title,
424            item_category: clickedProp.metadata.rental_type,
425            item_location_id: clickedProp.province_code || undefined
426          }]
427        });
428      }
429
430      var detailEl = document.querySelector("[data-analytics-property-detail]");
431      var propData = detailEl ? propertyPayload(detailEl) : {};
432
433      // 1. Bắt tất cả các loại nút CTA (Hotline, Zalo, Xem phòng...)
434      var cta = event.target.closest("[data-analytics-cta], .support-item.hotline, .support-item.zalo, .btn-zalo, a[href^='tel:'], a[href*='zalo.me']");
435      if (cta) {
436        var ctaName = cleanText(cta.getAttribute("data-analytics-cta") || cta.textContent || cta.className || "cta");
437        if (cta.closest(".support-item.hotline, a[href^='tel:']")) ctaName = "hotline";
438        if (cta.closest(".support-item.zalo, .btn-zalo, a[href*='zalo.me']")) ctaName = "chat_zalo";
439
440        track("cta_clicked", Object.assign({
441          metric_key: ctaName.slice(0, 120),
442          entity_type: "cta",
443          entity_id: ctaName.slice(0, 120)
444        }, propData));
445      }
446
447      // 2. Bắt tất cả các loại nút Kiểm tra phòng trống (Desktop, Mobile sheet, Sticky header)
448      var checkAvailBtn = event.target.closest("#btn-check-avail, .js-header-btn-check-avail, .btn-check-avail-header, .btn-check-avail, .js-sheet-apply-btn, [data-check-avail]");
449      if (checkAvailBtn) {
450        track("date_selected", Object.assign({
451          metric_key: "booking_date_selected"
452        }, propData));
453      }
454
455      // 3. Bắt tất cả các loại nút Đặt phòng (Đặt ngay, Đặt nguyên căn, Đặt phòng lẻ, Sticky mobile)
456      var bookBtn = event.target.closest(".btn-booking, .bookNow, .btn-booking-room, .btn-booking-wh
456ole, .btn-booking-villa, .btn-booking-pro, #btn-booking, [data-booking-btn]");
457      if (bookBtn) {
458        track("booking_started", Object.assign({
459          metric_key: "booking_started"
460        }, propData));
461
462        sendGa4("begin_checkout", {
463          currency: "VND",
464          value: propData.price || undefined,
465          items: propData.property_id ? [{
466            item_id: String(propData.property_id || ""),
467            item_name: propData.metadata && propData.metadata.title,
468            item_category: propData.metadata && propData.metadata.rental_type,
469            item_location_id: propData.province_code || undefined,
470            price: propData.price || undefined,
471            quantity: 1
472          }] : []
473        });
474      }
475    }, { passive: true });
476  }
477
478  function setupSearchNoResults() {
479    if (window.jQuery) {
480      window.jQuery(document).on("hostingSearchManager:searchCompleted.analytics", function (_, totalCount) {
481        if (Number(totalCount) === 0) {
482          var params = new URLSearchParams(window.location.search || "");
483          var key = window.location.pathname + "|" + (params.get("keyword") || "");
484          if (sentNoResults[key]) return;
485          sentNoResults[key] = true;
486          track("search_no_results", {
487            search_keyword: cleanText(params.get("keyword") || params.get("location")),
488            province_code: cleanText(params.get("province")),
489            entity_type: "supply_gap"
490          });
491        }
492      });
493    }
494  }
495
496  function inferInitialEvents() {
497    track("page_view");
498
499    var detail = document.querySelector("[data-analytics-property-detail]");
500    if (detail) {
501      var propData = propertyPayload(detail);
502      track("listing_detail_viewed", propData);
503      sendGa4("view_item", {
504        currency: "VND",
505        value: propData.price || undefined,
506        items: [{
507          item_id: String(propData.property_id || ""),
508          item_name: propData.metadata.title,
509          item_category: propData.metadata.rental_type,
510          item_location_id: propData.province_code || undefined,
511          price: propData.price || undefined,
512          quantity: 1
513        }]
514      });
515    }
516
517    if (window.location.pathname.indexOf("/tim-kiem") === 0 || document.querySelector("[data-analytics-page-kind]")) {
518      var params = new URLSearchParams(window.location.search || "");
519      var kw = cleanText(params.get("keyword") || params.get("location"));
520      var prov = cleanText(params.get("province"));
521      track("search_results_viewed", {
522        search_keyword: kw,
523        province_code: prov,
524        metadata: { checkin: params.get("checkin"), checkout: params.get("checkout"), guests: params.get("person") }
525      });
526      if (kw || prov) {
527        sendGa4("view_search_results", {
528          search_term: kw || prov,
529          search_location: prov || undefined
530        });
531      }
532    }
533  }
534
535  window.OhdidiAnalytics = { track: track };
536
537  window.addEventListener("pagehide", function () { flushQueue(true); });
538  document.addEventListener("visibilitychange", function () {
539    if (document.visibilityState === "hidden") flushQueue(true);
540  });
541
542  document.addEventListener("DOMContentLoaded", function () {
543    inferInitialEvents();
544    setupSearchSubmit();
545    setupClicks();
546    setupSearchNoResults();
547  });
548})(window, document);

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.