1(() => { 2 'use strict'; 3 4 if (typeof window.tunAnalyticsTrack !== 'function') return; 5 6 const main = document.querySelector('main'); 7 if (!main) return; 8 9 const schemaVersion = 'content_v2'; 10 const registered = new WeakSet(); 11 const videoBound = new WeakSet(); 12 const targets = new Map(); 13 const visibleTargets = new Set(); 14 const heatmapMilestones = new Set([2, 5, 10, 20, 40, 60, 120]); 15 const downloadExtensionPattern = /\.(?:7z|apk|appimage|csv|deb|dmg|docx?|exe|gz|ino|ipa|iso|msi|odp|ods|odt|pdf|pkg|pptx?|rar|rpm|tar|tgz|txt|xlsx?|zip)$/i; 16 const isMeasuredScope = (element) => element.closest('main') === main 17 || Boolean(element.closest('[data-analytics-sticky]')); 18 19 const safeKey = (value, fallback) => { 20 const normalized = String(value || '') 21 .toLowerCase() 22 .normalize('NFD') 23 .replace(/[\u0300-\u036f]/g, '') 24 .replace(/[^a-z0-9]+/g, '_') 25 .replace(/^_+|_+$/g, '') 26 .slice(0, 80); 27 28 return normalized || fallback; 29 }; 30 const track = (name, target, properties = {}) => window.tunAnalyticsTrack(name, { 31 analytics_schema_version: schemaVersion, 32 interaction_target: target, 33 ...properties, 34 }); 35 const mediaKey = (element, fallback) => { 36 const explicit = element.dataset.analyticsMedia 37 || element.closest?.('[data-analytics-media]')?.dataset.analyticsMedia; 38 if (explicit) return safeKey(explicit, fallback); 39 40 const sourceElement = element.matches?.('img, video, iframe') 41 ? element 42 : element.querySelector?.('img, video, iframe'); 43 try { 44 const source = sourceElement?.currentSrc 45 || sourceElement?.src 46 || sourceElement?.getAttribute?.('src') 47 || getComputedStyle(element).backgroundImage.match(/url\(["']?([^"')]+)["']?\)/)?.[1] 48 || ''; 49 const filename = new URL(source, window.location.href).pathname.split('/').pop()?.replace(/\.[a-z0-9]+$/i, ''); 50 return safeKey(filename, fallback); 51 } catch (_error) { 52 return fallback; 53 } 54 }; 55 const sectionKey = (section, index) => { 56 const heading = section.querySelector(':scope > h1, :scope > h2, :scope > h3, :scope header h1, :scope header h2, :scope header h3'); 57 const labelledBy = section.getAttribute('aria-labelledby'); 58 const label = section.dataset.analyticsSection 59 || section.id 60 || (labelledBy ? document.getElementById(labelledBy)?.textContent : '') 61 || heading?.textContent; 62 63 return `${String(index + 1).padStart(2, '0')}_${safeKey(label, 'section')}`.slice(0, 80); 64 }; 65 const clampPercent = (value) => Math.max(0, Math.min(100, Math.round(value * 100) / 100)); 66 const downloadContext = (link, url) => { 67 const container = link.closest?.('[data-download-kind], [data-download-product-key], [data-download-asset-key]'); 68 const source = { ...container?.dataset, ...link.dataset }; 69 const filename = decodeURIComponent(url.pathname.split('/').pop() || '').slice(0, 120); 70 const extension = filename.includes('.') ? filename.split('.').pop().toLowerCase().slice(0, 12) : ''; 71 const pagePath = window.location.pathname; 72 const inferredKind = /\/apps(?:\/|$)/.test(pagePath) || ['apk', 'appimage', 'deb', 'dmg', 'exe', 'ipa', 'msi', 'pkg', 'rpm'].includes(extension) 73 ? 'software' 74 : /\/lab\/(?:pubblicazioni|papers)(?:\/|$)/.test(pagePath) || container?.classList?.contains('paper-record') 75 ? 'paper' 76 : extension === 'ino' 77 ? 'firmware' 78 : 'file'; 79 const versionMatch = filename.match(/(?:^|[-_\s])v?(\d+(?:\.\d+){1,3}(?:[-_]\d+)?)(?=[-_.\s]|$)/i); 80 const productKey = source.downloadProductKey || ''; 81 const assetName = source.downloadAssetName || source.downloadProductName || filename.replace(/\.[^.]+$/, '') || 'download'; 82 83 return { 84 asset_kind: safeKey(source.downloadKind || inferredKind, 'file'), 85 asset_key: safeKey(source.downloadAssetKey || productKey || filename, 'download'),
86 asset_name: String(assetName).slice(0, 120), 87 asset_version: String(source.downloadVersion || versionMatch?.[1] || '').slice(0, 40), 88 asset_host: url.hostname.slice(0, 120), 89 asset_path: url.pathname.slice(0, 240), 90 file_extension: extension, 91 }; 92 }; 93 const elementPath = (element) => { 94 const parts = []; 95 let current = element; 96 97 while (current && current !== document.body && parts.length < 5) { 98 const tag = current.tagName?.toLowerCase(); 99 if (!tag) break; 100 101 if (current.id) { 102 parts.unshift(`${tag}_${safeKey(current.id, 'element')}`); 103 break; 104 } 105 106 const siblings = current.parentElement 107 ? [...current.parentElement.children].filter((sibling) => sibling.tagName === current.tagName) 108 : []; 109 const position = siblings.length > 1 ? `_${siblings.indexOf(current) + 1}` : ''; 110 parts.unshift(`${tag}${position}`); 111 current = current.parentElement; 112 113 if (current?.tagName?.toLowerCase() === 'main') break; 114 } 115 116 return parts.join('_'); 117 }; 118 const elementKey = (element) => { 119 const explicit = element.closest?.('[data-analytics-action], [data-analytics-media], [data-commerce-target], [data-cf-target]'); 120 const explicitValue = explicit?.dataset.analyticsAction 121 || explicit?.dataset.analyticsMedia 122 || explicit?.dataset.commerceTarget 123 || explicit?.dataset.cfTarget; 124 125 if (explicitValue) return `${explicit.tagName.toLowerCase()}_${safeKey(explicitValue, 'element')}`.slice(0, 120); 126 127 const interactive = element.closest?.('a[href], button, input, select, textarea, summary, [role="button"]'); 128 if (interactive?.id) return `${interactive.tagName.toLowerCase()}_${safeKey(interactive.id, 'element')}`.slice(0, 120); 129 130 const link = interactive?.matches?.('a[href]') ? interactive : null; 131 if (link) { 132 try { 133 const destination = new URL(link.getAttribute('href') || '', window.location.href); 134 const target = destination.origin === window.location.origin ? destination.pathname : destination.hostname; 135 return `a_${safeKey(target, 'link')}_${safeKey(elementPath(link), 'position')}`.slice(0, 120); 136 } catch (_error) { 137 return `a_link_${safeKey(elementPath(link), 'position')}`.slice(0, 120); 138 } 139 } 140 141 if (interactive) return safeKey(elementPath(interactive), interactive.tagName.toLowerCase()).slice(0, 120); 142 143 const section = element.closest?.('[data-analytics-section]'); 144 if (section?.dataset.analyticsSection) { 145 return `section_${safeKey(section.dataset.analyticsSection, 'section')}`.slice(0, 120); 146 } 147 148 const identified = element.closest?.('[id]'); 149 if (identified?.id) return `${identified.tagName.toLowerCase()}_${safeKey(identified.id, 'element')}`.slice(0, 120); 150 151 return safeKey(elementPath(element), element.tagName?.toLowerCase() || 'element').slice(0, 120); 152 }; 153 const geometry = (element, point = null) => { 154 const rect = element.getBoundingClientRect(); 155 const pageWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth || 0, window.innerWidth, 1); 156 const pageHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0, window.innerHeight, 1); 157 const clientX = Number.isFinite(point?.clientX) && point.clientX > 0 ? point.clientX : rect.left + (rect.width / 2); 158 const clientY = Number.isFinite(point?.clientY) && point.clientY > 0 ? point.clientY : rect.top + (rect.height / 2); 159 160 return { 161 element_x_percent: clampPercent(((clientX - rect.left) / Math.max(rect.width, 1)) * 100), 162 element_y_percent: clampPercent(((clientY - rect.top) / Math.max(rect.height, 1)) * 100), 163 page_x_percent: clampPercent(((clientX + window.scrollX) / pageWidth) * 100), 164 page_y_percent: clampPercent(((clientY + window.scrollY) / pageHeight) * 100), 165 viewport_width: Math.round(window.innerWidth), 166 viewport_height: Math.round(window.innerHeight), 167 page_width: Math.round(pageWidth), 168 page_height: Math.round(pageHeight), 169 }; 170 }; 171 172 const attentionObserver = 'IntersectionObserver' in window 173 ? new IntersectionObserver((entries) => {
174 entries.forEach((entry) => { 175 const requiredPixels = Math.min(120, entry.boundingClientRect.height * .25); 176 if (entry.isIntersecting && entry.intersectionRect.height >= requiredPixels && document.visibilityState !== 'hidden') { 177 visibleTargets.add(entry.target); 178 } else { 179 visibleTargets.delete(entry.target); 180 } 181 }); 182 }, { threshold: [0, .25, .5] }) 183 : null; 184 185 const registerAttention = (element, key, type, milestones, properties = {}) => { 186 if (registered.has(element)) return; 187 188 registered.add(element); 189 element.dataset.analyticsMeasured = 'true'; 190 targets.set(element, { key, type, milestones, properties, seconds: 0, sent: new Set() }); 191 attentionObserver?.observe(element); 192 }; 193 const bindVideo = (video, target) => { 194 if (videoBound.has(video)) return; 195 videoBound.add(video); 196 197 const milestones = new Set(); 198 video.addEventListener('play', () => { 199 if (milestones.has('played')) return; 200 milestones.add('played'); 201 track('video_played', target, { content_type: 'video' }); 202 }); 203 video.addEventListener('timeupdate', () => { 204 if (!Number.isFinite(video.duration) || video.duration <= 0) return; 205 const progress = Math.floor((video.currentTime / video.duration) * 100); 206 [25, 50, 75].forEach((milestone) => { 207 if (progress < milestone || milestones.has(milestone)) return; 208 milestones.add(milestone); 209 track('video_progress', target, { content_type: 'video', percent: milestone }); 210 }); 211 }); 212 video.addEventListener('ended', () => { 213 if (milestones.has(100)) return; 214 milestones.add(100); 215 track('video_progress', target, { content_type: 'video', percent: 100 }); 216 }); 217 }; 218 219 const registerTree = (root) => { 220 if (!(root instanceof Element)) return; 221 const query = (selector) => [ 222 ...(root.matches(selector) ? [root] : []), 223 ...root.querySelectorAll(selector), 224 ]; 225 226 const sections = query('section, article[id], [data-analytics-section]') 227 .filter((section, index, list) => isMeasuredScope(section) && list.indexOf(section) === index); 228 const allSections = [...main.querySelectorAll('section, article[id], [data-analytics-section]')]; 229 sections.forEach((section) => { 230 const key = sectionKey(section, Math.max(0, allSections.indexOf(section))); 231 section.dataset.analyticsSection ||= key; 232 registerAttention(section, key, 'section', [ 233 { name: 'content_section_viewed', seconds: 2 }, 234 { name: 'content_section_engaged', seconds: 8 }, 235 ]); 236 }); 237 238 const candidates = query('img:not([data-analytics-ignore]), video, iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"], [data-analytics-media]'); 239 const mediaElements = [...new Set(candidates.map((candidate) => candidate.closest('[data-analytics-media]') || candidate))] 240 .filter((element) => isMeasuredScope(element)); 241 242 mediaElements.forEach((element, index) => { 243 if (registered.has(element)) return; 244 const nativeVideo = element.matches('video') ? element : element.querySelector('video'); 245 const embeddedVideo = element.matches('iframe') ? element : element.querySelector('iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"]'); 246 const image = element.matches('img') ? element : element.querySelector('img'); 247 const type = nativeVideo ? 'video' : embeddedVideo ? 'embedded_video' : 'image'; 248 249 if (type === 'image' && !element.dataset.analyticsMedia) { 250 const box = (image || element).getBoundingClientRect(); 251 if (box.width < 160 || box.height < 100) { 252 image?.addEventListener('load', () => registerTree(element), { once: true }); 253 return; 254 } 255 } 256
257 const key = mediaKey(element, `${type}_${index + 1}`); 258 element.dataset.analyticsMedia ||= key; 259 registerAttention(element, key, type, type === 'embedded_video' 260 ? [{ name: 'video_embed_viewed', seconds: 2 }] 261 : type === 'image' 262 ? [{ name: 'image_viewed', seconds: 2 }, { name: 'image_engaged', seconds: 8 }] 263 : []); 264 if (nativeVideo) bindVideo(nativeVideo, key); 265 }); 266 267 query('a[href], button, [role="button"], [data-inquiry-open], [data-lightbox-item]') 268 .filter((element) => isMeasuredScope(element)) 269 .forEach((element) => { 270 element.dataset.analyticsAction ||= elementKey(element); 271 let actionRole = element.matches('[data-inquiry-open], [data-commerce-target], [data-cf-target]') 272 ? 'commercial' 273 : 'navigation'; 274 const href = element.getAttribute('href') || ''; 275 if (href) { 276 try { 277 const destination = new URL(href, window.location.href); 278 const isDirectContact = ['mailto:', 'tel:'].includes(destination.protocol) 279 || /wa\.me|whatsapp\.com|apps\.apple\.com|play\.google\.com/i.test(destination.hostname); 280 const isDownload = element.hasAttribute('download') 281 || element.hasAttribute('data-analytics-download') 282 || downloadExtensionPattern.test(destination.pathname); 283 const isCommercialPath = destination.origin === window.location.origin 284 && destination.pathname !== window.location.pathname 285 && !href.startsWith('#'); 286 if (isDirectContact || isDownload || isCommercialPath) actionRole = 'commercial'; 287 } catch (_error) { 288 actionRole = 'navigation'; 289 } 290 } 291 registerAttention(element, element.dataset.analyticsAction, 'action', [ 292 { name: 'content_action_viewed', seconds: 2 }, 293 ], { action_role: actionRole }); 294 }); 295 }; 296 297 registerTree(main); 298 document.querySelectorAll('[data-analytics-sticky]').forEach(registerTree); 299 300 const pageType = main.dataset.growthPageType || (/^\/(?:it|en-it|it-ch|fr-ch|de-ch|en-ch)\/?$/.test(window.location.pathname) 301 ? 'homepage' 302 : document.querySelector('[data-inquiry-form], [data-inquiry-open], [data-commerce-surface]') 303 ? 'commercial' 304 : 'content'); 305 track('content_page_view', pageType, { 306 page_type: pageType, 307 section_count: main.querySelectorAll('[data-analytics-section]').length, 308 image_count: main.querySelectorAll('img').length, 309 video_count: main.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="vimeo"]').length, 310 }); 311 track('heatmap_page_ready', pageType, { 312 page_type: pageType, 313 measured_element_count: targets.size, 314 }); 315 316 const dynamicContentObserver = new MutationObserver((mutations) => { 317 mutations.forEach((mutation) => { 318 mutation.addedNodes.forEach((node) => { 319 if (node instanceof Element) registerTree(node); 320 }); 321 }); 322 }); 323 dynamicContentObserver.observe(main, { childList: true, subtree: true }); 324 325 const heatmapTimer = window.setInterval(() => { 326 if (document.visibilityState === 'hidden') return; 327 328 visibleTargets.forEach((element) => { 329 const state = targets.get(element); 330 if (!state) return; 331 332 state.seconds += 1; 333 state.milestones.forEach(({ name, seconds }) => { 334 if (state.seconds !== seconds || state.sent.has(name)) return; 335 state.sent.add(name); 336 track(name, state.key, { content_type: state.type, engagement_seconds: seconds, ...state.properties }); 337 }); 338 339 if (heatmapMilestones.has(state.seconds)) { 340 track('heatmap_attention', state.key, { 341 content_type: state.type, 342 interval_seconds: state.seconds, 343 ...geometry(element), 344 }); 345 } 346 }); 347 }, 1000); 348 349 let lastClick = null; 350 document.addEventListener('click', (event) => { 351 const element = event.target.closest?.('main *, [data-analytics-sticky], [data-analytics-sticky] *'); 352 if (!element) return; 353
354 const key = elementKey(element); 355 const now = window.performance.now(); 356 const clientX = Number.isFinite(event.clientX) ? event.clientX : 0; 357 const clientY = Number.isFinite(event.clientY) ? event.clientY : 0; 358 const distance = lastClick ? Math.hypot(clientX - lastClick.clientX, clientY - lastClick.clientY) : Number.POSITIVE_INFINITY; 359 const sameSequence = lastClick && now - lastClick.time <= 800 && (lastClick.key === key || distance <= 48); 360 const sequence = sameSequence ? lastClick.sequence + 1 : 1; 361 lastClick = { time: now, clientX, clientY, key, sequence }; 362 363 const interactive = Boolean(element.closest('a[href], button, input, select, textarea, summary, [role="button"], [data-inquiry-open], [data-lightbox-item]')); 364 track('heatmap_click', key, { 365 element_tag: element.tagName.toLowerCase(), 366 click_kind: interactive ? 'interactive' : 'dead', 367 click_sequence: sequence, 368 rapid_click: sequence >= 3, 369 ...geometry(element, event), 370 }); 371 }); 372 373 let maximumScrollDepth = 0; 374 const updateMaximumScrollDepth = () => { 375 const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight; 376 maximumScrollDepth = scrollableHeight <= 0 377 ? 100 378 : Math.max(maximumScrollDepth, clampPercent((window.scrollY / scrollableHeight) * 100)); 379 }; 380 let scrollScheduled = false; 381 window.addEventListener('scroll', () => { 382 if (scrollScheduled) return; 383 scrollScheduled = true; 384 window.requestAnimationFrame(() => { 385 scrollScheduled = false; 386 updateMaximumScrollDepth(); 387 }); 388 }, { passive: true }); 389 window.addEventListener('pagehide', () => { 390 updateMaximumScrollDepth(); 391 track('heatmap_scroll', pageType, { 392 max_scroll_percent: maximumScrollDepth, 393 viewport_width: Math.round(window.innerWidth), 394 viewport_height: Math.round(window.innerHeight), 395 page_width: Math.round(Math.max(document.documentElement.scrollWidth, window.innerWidth)), 396 page_height: Math.round(Math.max(document.documentElement.scrollHeight, window.innerHeight)), 397 }); 398 window.clearInterval(heatmapTimer); 399 dynamicContentObserver.disconnect(); 400 attentionObserver?.disconnect(); 401 }, { once: true }); 402 updateMaximumScrollDepth(); 403 404 document.addEventListener('click', (event) => { 405 const link = event.target.closest?.('main a[href], main button[data-inquiry-open], main [data-lightbox-item], [data-analytics-sticky] a[href], [data-analytics-sticky] button[data-inquiry-open], [data-analytics-sticky] [data-lightbox-item]'); 406 if (!link) return; 407 408 const href = link.getAttribute('href') || ''; 409 let kind = link.matches('[data-inquiry-open]') ? 'inquiry' : 'action'; 410 let target = link.dataset.analyticsAction || link.dataset.cfTarget || link.dataset.commerceTarget || ''; 411 let destinationPath = ''; 412 413 if (href) { 414 try { 415 const url = new URL(href, window.location.href); 416 destinationPath = url.origin === window.location.origin ? url.pathname : ''; 417 if (url.protocol === 'mailto:') { 418 kind = 'email'; 419 target ||= 'email'; 420 } else if (url.protocol === 'tel:') { 421 kind = 'phone'; 422 target ||= 'phone'; 423 } else if (/wa\.me$|whatsapp\.com$/i.test(url.hostname)) { 424 kind = 'whatsapp'; 425 target ||= 'whatsapp'; 426 } else if (/apps\.apple\.com$|play\.google\.com$/i.test(url.hostname)) { 427 kind = 'store'; 428 target ||= url.hostname; 429 } else if (link.hasAttribute('download') 430 || link.hasAttribute('data-analytics-download') 431 || downloadExtensionPattern.test(url.pathname)) { 432 kind = 'download'; 433 target ||= url.pathname; 434 destinationPath = url.pathname; 435 } else if (url.origin !== window.location.origin) { 436 kind = 'external'; 437 target ||= url.hostname; 438 } else { 439 kind = 'internal'; 440 target ||= url.pathname; 441 } 442 } catch (_error) { 443 target ||= 'action'; 444 } 445 } 446 447 const actionTarget = safeKey(target, 'action'); 448 const internalCommercialPath = kind === 'internal' 449 && destinationPath 450 && destinationPath !== window.location.pathname 451 && !href.startsWith('#'); 452 const actionRole = ['email', 'phone', 'whatsapp', 'inquiry', 'download', 'store'].includes(kind) 453 || Boolean(link.dataset.commerceTarget || link.dataset.cfTarget) 454 || internalCommercialPath 455 ? 'commercial' 456 : 'navigation'; 457 458 track('content_action_clicked', actionTarget, { 459 action_type: kind, 460 action_role: actionRole, 461 destination_path: destinationPath, 462 }); 463 464 if (kind === 'download') { 465 const url = new URL(href, window.location.href); 466 track('download_started', actionTarget, { 467 ...downloadContext(link, url), 468 delivery_channel: 'direct', 469 confirmation: 'click', 470 }); 471 } else if (kind === 'store') { 472 const url = new URL(href, window.location.href); 473 track('store_opened', actionTarget, { 474 ...downloadContext(link, url), 475 asset_kind: 'software', 476 store_host: url.hostname, 477 delivery_channel: /play\.google\.com$/i.test(url.hostname) ? 'play_store' : 'app_store', 478 confirmation: 'navigation', 479 }); 480 } 481 }); 482})();
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.