PageSourceSearch

https://ghidulprimariilor.ro/static/landing_pages/js/analytics.js

js ghidulprimariilor.ro collected 2026-10-02 08:32:46 UTC 6,297 bytes, 231 lines download raw bytes

1/**
2 * Landing Pages Analytics
3 *
4 * Tracks user interactions on landing pages including:
5 * - Page views and time on page
6 * - CTA clicks
7 * - Form submissions
8 * - Scroll depth
9 * - Element visibility
10 */
11
12(function() {
13  'use strict';
14
15  // Configuration
16  const CONFIG = {
17    trackingEndpoint: '/landing/api/track/',
18    debounceTime: 250,
19    scrollThresholds: [25, 50, 75, 100],
20    visibilityThreshold: 0.5,
21  };
22
23  // State
24  const state = {
25    pageLoadTime: Date.now(),
26    scrollDepthTracked: new Set(),
27    visibleZones: new Set(),
28    pageId: null,
29    variantId: null,
30  };
31
32  // Initialize
33  function init() {
34    const pageEl = document.querySelector('.landing-page');
35    if (!pageEl) return;
36
37    state.pageId = pageEl.dataset.pageId;
38    state.variantId = pageEl.dataset.variantId || null;
39
40    // Track page view
41    trackEvent('page_view', { page_id: state.pageId, variant_id: state.variantId });
42
43    // Set up event listeners
44    setupClickTracking();
45    setupFormTracking();
46    setupScrollTracking();
47    setupVisibilityTracking();
48    setupUnloadTracking();
49  }
50
51  // Track event
52  function trackEvent(eventType, data = {}) {
53    if (!state.pageId) return;  // Don't track if no page ID
54
55    const payload = {
56      event_type: eventType,
57      landing_page_id: parseInt(state.pageId, 10),
58      variant_id: state.variantId ? parseInt(state.variantId, 10) : null,
59      timestamp: new Date().toISOString(),
60      url: window.location.href,
61      referrer: document.referrer,
62      metadata: data,
63    };
64
65    // Use sendBeacon for reliability
66    if (navigator.sendBeacon) {
67      const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
68      navigator.sendBeacon(CONFIG.trackingEndpoint, blob);
69    } else {
70      // Fallback to fetch
71      fetch(CONFIG.trackingEndpoint, {
72        method: 'POST',
73        headers: {
74          'Content-Type': 'application/json',
75          'X-CSRFToken': getCsrfToken(),
76        },
77        body: JSON.stringify(payload),
78        keepalive: true,
79      }).catch(() => {});
80    }
81  }
82
83  // Get CSRF token
84  function getCsrfToken() {
85    const cookieValue = document.cookie
86      .split('; ')
87      .find(row => row.startsWith('csrftoken='));
88    return cookieValue ? cookieValue.split('=')[1] : '';
89  }
90
91  // Click tracking
92  function setupClickTracking() {
93    document.addEventListener('click', function(e) {
94      const target = e.target.closest('[data-event]');
95      if (!target) return;
96
97      const eventType = target.dataset.event;
98      const zoneId = target.dataset.zoneId;
99      const data = {
100        zone_id: zoneId,
101        element_text: target.textContent.trim().substring(0, 100),
102        element_href: target.href || null,
103      };
104
105      // Include any additional data-* attributes
106      for (const [key, value] of Object.entries(target.dataset)) {
107        if (key !== 'event' && key !== 'zoneId') {
108          data[key] = value;
109        }
110      }
111
112      trackEvent(eventType, data);
113    });
114  }
115
116  // Form tracking
117  function setupFormTracking() {
118    document.addEventListener('submit', function(e) {
119      const form = e.target;
120      const zoneId = form.dataset.zoneId;
121
122      if (!zoneId) return;
123
124      trackEvent('form_submit', {
125        zone_id: zoneId,
126        form_id: form.id || null,
127      });
128    });
129
130    // Track form field focus
131    document.addEventListener('focus', function(e) {
132      const input = e.target;
133      if (!input.name) return;
134
135      const form = input.closest('form[data-zone-id]');
136      if (!form) return;
137
138      trackEvent('form_field_focus', {
139        zone_id: form.dataset.zoneId,
140        field_name: input.name,
141      });
142    }, true);
143  }
144
145  // Scroll tracking
146  function setupScrollTracking() {
147    let ticking = false;
148
149    function checkScrollDepth() {
150      const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
151      const scrollPercent = scrollHeight > 0 ? Math.round((window.scrollY / scrollHeight) * 100) : 100;
152
153      for (const threshold of CONFIG.scrollThresholds) {
154        if (scrollPercent >= threshold && !state.scrollDepthTracked.has(threshold)) {
155          state.scrollDepthTracked.add(threshold);
156          trackEvent('scroll_depth', { depth: threshold });
157        }
158      }
159      ticking = false;
160    }
161
162    window.addEventListener('scroll', function() {
163      if (!ticking) {
164        requestAnimationFrame(checkScrollDepth);
165        ticking = true;
166      }
167    }, { passive: true });
168  }
169
170  // Visibility tracking using Intersection Observer
171  function setupVisibilityTracking() {
172    if (!('IntersectionObserver' in window)) return;
173
174    const observer = new IntersectionObserver(
175      (entries) => {
176        entries.forEach((entry) => {
177          const zoneId = entry.target.dataset.zoneId;
178          if (!zoneId) return;
179
180          if (entry.isIntersecting && !state.visibleZones.has(zoneId)) {
181            state.visibleZones.add(zoneId);
182            trackEvent('zone_visible', {
183              zone_id: zoneId,
184              zone_type: entry.target.dataset.zoneType,
185            });
186          }
187        });
188      },
189      { threshold: CONFIG.visibilityThreshold }
190    );
191
192    // Observe all zones
193    document.querySelectorAll('[data-zone-id]').forEach((zone) => {
194      observer.observe(zone);
195    });
196  }
197
198  // Unload tracking
199  function setupUnloadTracking() {
200    window.addEventListener('beforeunload', function() {
201      const timeOnPage = Math.round((Date.now() - state.pageLoadTime) / 1000);
202      trackEvent('page_exit', {
203        time_on_page: timeOnPage,
204        scroll_depth: Math.max(...state.scrollDepthTracked, 0),
205        zones_viewed: state.visibleZones.size,
206      });
207    });
208
209    // Also track visibility change (mobile)
210    document.addEventListener('visibilitychange', function() {
211      if (document.visibilityState === 'hidden') {
212        const timeOnPage = Math.round((Date.now() - state.pageLoadTime) / 1000);
213        trackEvent('page_hidden', {
214          time_on_page: timeOnPage,
215        });
216      }
217    });
218  }
219
220  // Initialize when DOM is ready
221  if (document.readyState === 'loading') {
222    document.addEventListener('DOMContentLoaded', init);
223  } else {
224    init();
225  }
226
227  // Expose for external use
228  window.LandingPageAnalytics = {
229    trackEvent: trackEvent,
230  };
231})();

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.