PageSourceSearch

https://podhoc.com/js/analytics.js

js podhoc.com collected 2026-10-02 15:52:08 UTC 15,000 bytes, 397 lines download raw bytes

1/**
2 * Podhoc – Presentational Web GA4 Event Tracking
3 *
4 * Provides declarative (data-attribute) and programmatic event tracking.
5 *
6 * Declarative usage — add attributes to any element:
7 *   <a href="/pricing"
8 *      data-ga-event="cta_click"
9 *      data-ga-category="engagement"
10 *      data-ga-label="nav_pricing">Pricing</a>
11 *
12 * The script auto-binds click listeners via event delegation so it works for
13 * elements rendered after DOMContentLoaded (e.g. cookie banner, modals).
14 *
15 * All calls are wrapped in try/catch so analytics failures never break the site.
16 */
17
18(function () {
19  'use strict';
20
21  // Consent / Do-Not-Track gate -----------------------------------------
22  //   Respect navigator.doNotTrack and the consent decision stored by the
23  //   cookie banner in main.js. Source-of-truth order:
24  //     1. Do-Not-Track browser signal (hard precedence — never tracks).
25  //     2. Shared `podhoc_consent` cookie (cross-subdomain, written by both
26  //        www and app.podhoc.com). Format:
27  //        `v1:analytics=<0|1>,marketing=<0|1>,ts=<unix-seconds>`.
28  //     3. Legacy per-origin localStorage preference (backward compat).
29  var PREFS_KEY = 'podhoc_cookie_preferences';
30
31  function isTrackingAllowed() {
32    // Respect Do-Not-Track browser signal (unchanged precedence)
33    if (
34      navigator.doNotTrack === '1' ||
35      (window.doNotTrack && window.doNotTrack === '1') ||
36      navigator.msDoNotTrack === '1'
37    ) {
38      return false;
39    }
40    // Shared consent cookie wins over localStorage when present. Parsed with
41    // the STRICT shared parser (window.podhocParseConsent, consent-parse.js) so
42    // this surface accepts/rejects EXACTLY what the webapp's parseConsentValue
43    // does — never grant where the webapp denies. A garbled / unknown-key
44    // cookie → null → fall through to the legacy localStorage check.
45    try {
46      var c = document.cookie.match(/(?:^|; )podhoc_consent=([^;]+)/);
47      if (c && typeof window.podhocParseConsent === 'function') {
48        var parsed = window.podhocParseConsent(decodeURIComponent(c[1]));
49        if (parsed) return parsed.analytics === true;
50      }
51    } catch {
52      /* ignore */
53    }
54    // Legacy localStorage fallback (analytics must be opted-in)
55    try {
56      var prefs = JSON.parse(localStorage.getItem(PREFS_KEY) || '{}');
57      if (prefs.analytics === false) return false;
58    } catch {
59      /* ignore */
60    }
61    return true;
62  }
63
64  // Safe gtag wrapper ---------------------------------------------------
65  //
66  // A/B enrichment: baseof.html's <head> inlines
67  // assets/js/experiment-event-params.js, which installs
68  // window.__podhocAbParams — the SAME helper baseof.html's own funnel-event
69  // IIFE calls as ab(). Routing this file's fourteen events through it too
70  // means EVERY marketing event (faq_toggle, sample_play, nav_click,
71  // pricing_plan_click, extension_download_click, …) carries the active
72  // assignment: flat experiment_id/variant_id with one experiment running,
73  // the composite `experiments` param with several. Before this, only the
74  // three events defined in baseof.html were enriched, so every other event
75  // was readable per-variant ONLY through a user-scoped funnel — correct, but
76  // it ruled out the event-level quick table for most experiments.
77  //
78  // Guarded, not assumed: the helper ships inside the prod-only GA block, so
79  // in dev (and if the head script ever fails) this degrades to exactly the
80  // previous behaviour. Never throws — the whole call is inside the try below.
81  function sendEvent(eventName, params) {
82    try {
83      if (!isTrackingAllowed()) return;
84      if (typeof gtag === 'function') {
85        var enriched = params || {};
86        if (typeof window.__podhocAbParams === 'function') {
87          enriched = window.__podhocAbParams(enriched);
88        }
89        gtag('event', eventName, enriched);
90      }
91    } catch {
92      /* analytics must never break the site */
93    }
94  }
95
96  // =====================================================================
97  // 1. Declarative data-attribute tracking (event delegation)
98  // =====================================================================
99  document.addEventListener(
100    'click',
101    function (e) {
102      // Walk up to find the closest element with data-ga-event
103      var target = e.target;
104      while (target && target !== document) {
105        if (target.getAttribute && target.getAttribute('data-ga-event')) {
106          sendEvent(target.getAttribute('data-ga-event'), {
107            event_category:
108              target.getAttribute('data-ga-category') || 'general',
109            event_label: target.getAttribute('data-ga-label') || '',
110            link_url: target.href || target.getAttribute('href') || '',
111          });
112          return;
113        }
114        target = target.parentElement;
115      }
116    },
117    { capture: true, passive: true },
118  );
119
120  // =====================================================================
121  // 2–10. All DOMContentLoaded setup consolidated into a single listener
122  // =====================================================================
123  document.addEventListener('DOMContentLoaded', function () {
124    // --- 2. Navigation link tracking (header + mobile menu + footer) ---
125
126    // Header nav links
127    var navLinks = document.querySelectorAll('.header__nav-link');
128    navLinks.forEach(function (link) {
129      link.addEventListener('click', function () {
130        // Skip if already handled by declarative data-attribute tracking
131        if (link.hasAttribute('data-ga-event')) return;
132        sendEvent('nav_click', {
133          event_category: 'navigation',
134          event_label: link.textContent.trim(),
135          link_url: link.href || '',
136        });
137      });
138    });
139
140    // Mobile menu links
141    var mobileLinks = document.querySelectorAll('.mobile-menu a');
142    mobileLinks.forEach(function (link) {
143      link.addEventListener('click', function () {
144        if (link.hasAttribute('data-ga-event')) return;
145        sendEvent('nav_click', {
146          event_category: 'navigation',
147          event_label: 'mobile_' + link.textContent.trim(),
148          link_url: link.href || '',
149        });
150      });
151    });
152
153    // Footer links
154    var footerLinks = document.querySelectorAll('footer a');
155    footerLinks.forEach(function (link) {
156      link.addEventListener('click', function () {
157        if (link.hasAttribute('data-ga-event')) return;
158        sendEvent('footer_link_click', {
159          event_category: 'navigation',
160          event_label: link.textContent.trim(),
161          link_url: link.href || '',
162        });
163      });
164    });
165
166    // Social media links (detect by href containing known social domains)
167    var socialDomains = [
168      'twitter.com',
169      'x.com',
170      'linkedin.com',
171      'github.com',
172      'youtube.com',
173      'facebook.com',
174      'instagram.com',
175    ];
176    var allLinks = document.querySelectorAll('a[href]');
177    allLinks.forEach(function (link) {
178      var href = link.href || '';
179      socialDomains.forEach(function (domain) {
180        if (href.indexOf(domain) !== -1) {
181          link.addEventListener('click', function () {
182            if (link.hasAttribute('data-ga-event')) return;
183            sendEvent('social_link_click', {
184              event_category: 'social',
185              event_label: domain.replace('.com', ''),
186              link_url: href,
187            });
188          });
189        }
190      });
191    });
192
193    // --- 3. CTA / Get Started button tracking ---
194
195    // Primary CTA buttons (btn--accent in hero, pricing, etc.)
196    var ctaButtons = document.querySelectorAll(
197      '.hero__cta .btn, .pricing .btn, [data-cta]',
198    );
199    ctaButtons.forEach(function (btn) {
200      btn.addEventListener('click', function () {
201        if (btn.hasAttribute('data-ga-event')) return;
202        // Also skip anything the EXPERIMENT-AWARE emitter in baseof.html
203        // already handles. That handler keys on `data-ga-cta` and sends a
204        // stable semantic label (hero_start_creating, pricing_free, …) with
205        // the A/B params merged in; this one derives its label from the live
206        // DOM and merges nothing. Without this guard BOTH fire on every hero
207        // and pricing CTA click. Measured over 2026-07-21 -> 2026-09-04
208        // (docs/research/2026-09-04-ab-experiments-wave1-results.md §4.2):
209        // hero_start_creating 46 semantic events vs 46 text-labelled ones,
210        // hero_hear_examples 4 vs 4 — every such click counted twice.
211        //
212        // The duplicate was not merely redundant, it was corrupting:
213        //   - `textContent` on an A/B anchor returns BOTH arms fused, because
214        //     the CSS-toggle mechanism ships every variant in the DOM. Real
215        //     labels recorded: "Comenzar GratisReclama tus 50 créditos
216        //     gratis", "Inizia gratisRichiedi i tuoi 50 crediti gratis".
217        //   - The DOM is read at click time, so a browser-translated page
218        //     rewrites the label. GA4 holds a "免费开始" label on a site with
219        //     no zh locale.
220        // A label derived from rendered text is not a stable identifier.
221        if (btn.hasAttribute('data-ga-cta')) return;
222        sendEvent('cta_click', {
223          event_category: 'engagement',
224          event_label: btn.textContent.trim().substring(0, 60),
225          link_url: btn.href || btn.getAttribute('href') || '',
226        });
227      });
228    });
229
230    // --- 4. Language switcher tracking ---
231
232    var langSelectors = document.querySelectorAll('.header__lang');
233    langSelectors.forEach(function (select) {
234      select.addEventListener('change', function () {
235        sendEvent('language_switch', {
236          event_category: 'engagement',
237          event_label: select.value,
238        });
239      });
240    });
241
242    // --- 5. FAQ accordion tracking ---
243
244    var faqQuestions = document.querySelectorAll('.faq-item__question');
245    faqQuestions.forEach(function (question) {
246      question.addEventListener('click', function () {
247        if (question.hasAttribute('data-ga-event')) return;
248        var parent = question.closest('.faq-item');
249        var wasActive = parent && parent.classList.contains('active');
250        sendEvent('faq_toggle', {
251          event_category: 'engagement',
252          event_label: question.textContent.trim().substring(0, 80),
253          value: wasActive ? 0 : 1, // 1 = opening, 0 = closing
254        });
255      });
256    });
257
258    // --- 6. Sample audio player tracking ---
259
260    var playButtons = document.querySelectorAll('.sample-card__play-btn');
261    playButtons.forEach(function (btn) {
262      btn.addEventListener('click', function () {
263        if (btn.hasAttribute('data-ga-event')) return;
264        var card = btn.closest('.sample-card');
265        var style = card ? card.getAttribute('data-style') || '' : '';
266        sendEvent('sample_play', {
267          event_category: 'engagement',
268          event_label: style,
269        });
270      });
271    });
272
273    // Source selector (content source switch)
274    var sourceCards = document.querySelectorAll('.source-card');
275    sourceCards.forEach(function (card) {
276      card.addEventListener('click', function () {
277        if (card.hasAttribute('data-ga-event')) return;
278        sendEvent('source_select', {
279          event_category: 'engagement',
280          event_label: card.getAttribute('data-folder') || '',
281        });
282      });
283    });
284
285    // Example filter tabs
286    var filters = document.querySelectorAll('.examples__filter');
287    filters.forEach(function (filter) {
288      filter.addEventListener('click', function () {
289        if (filter.hasAttribute('data-ga-event')) return;
290        sendEvent('example_filter', {
291          event_category: 'engagement',
292          event_label: filter.getAttribute('data-filter') || '',
293        });
294      });
295    });
296
297    // --- 8. Cookie banner interaction tracking ---
298
299    var banner = document.querySelector('.cookie-banner');
300    if (banner) {
301      var actions = banner.querySelectorAll('[data-action]');
302      actions.forEach(function (btn) {
303        btn.addEventListener('click', function () {
304          // Defer the event send so the cookie banner handler stores the
305          // preference in localStorage before isTrackingAllowed() checks it.
306          setTimeout(function () {
307            sendEvent('cookie_consent', {
308              event_category: 'privacy',
309              event_label: btn.getAttribute('data-action') || '',
310            });
311          }, 0);
312        });
313      });
314    }
315
316    // --- 9. Pricing plan click tracking ---
317
318    var pricingButtons = document.querySelectorAll(
319      '.pricing-card .btn, .pricing-card__cta',
320    );
321    pricingButtons.forEach(function (btn) {
322      btn.addEventListener('click', function () {
323        if (btn.hasAttribute('data-ga-event')) return;
324        var card = btn.closest('.pricing-card');
325        var planName = card
326          ? (card.querySelector('.pricing-card__name') || {}).textContent || ''
327          : '';
328        sendEvent('pricing_plan_click', {
329          event_category: 'conversion',
330          event_label: planName.trim(),
331        });
332      });
333    });
334
335    // --- 10. Extension download/install button tracking ---
336
337    var extButtons = document.querySelectorAll(
338      '.extension-card .btn, .extension-card a, [data-extension-download]',
339    );
340    extButtons.forEach(function (btn) {
341      btn.addEventListener('click', function () {
342        if (btn.hasAttribute('data-ga-event')) return;
343        sendEvent('extension_download_click', {
344          event_category: 'conversion',
345          event_label: btn.textContent.trim().substring(0, 60),
346          link_url: btn.href || '',
347        });
348      });
349    });
350  });
351
352  // =====================================================================
353  // 7. Scroll depth tracking (25%, 50%, 75%, 100%)
354  // =====================================================================
355  (function () {
356    var thresholds = [25, 50, 75, 100];
357    var reached = {};
358    var ticking = false;
359
360    function getScrollPercent() {
361      var docHeight = Math.max(
362        document.body.scrollHeight,
363        document.documentElement.scrollHeight,
364      );
365      var winHeight = window.innerHeight;
366      var scrollTop = window.scrollY;
367      if (docHeight <= winHeight) return 100;
368      return Math.round((scrollTop / (docHeight - winHeight)) * 100);
369    }
370
371    function checkScroll() {
372      var pct = getScrollPercent();
373      thresholds.forEach(function (threshold) {
374        if (pct >= threshold && !reached[threshold]) {
375          reached[threshold] = true;
376          sendEvent('scroll_depth', {
377            event_category: 'engagement',
378            event_label: threshold + '%',
379            value: threshold,
380          });
381        }
382      });
383      ticking = false;
384    }
385
386    window.addEventListener(
387      'scroll',
388      function () {
389        if (!ticking) {
390          requestAnimationFrame(checkScroll);
391          ticking = true;
392        }
393      },
394      { passive: true },
395    );
396  })();
397})();

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.