From 92f949b3c60f85bbdf8c96c696e1582ddf4045b2 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Fri, 12 Dec 2025 18:30:37 +0100 Subject: [PATCH] Analytics/Tracking * drag the tracking object a decade into the future --- includes/components/pagetemplate.class.php | 28 +- setup/sql/updates/1765569409_01.sql | 1 + .../templates/global.js/announcement.js | 36 +- .../templates/global.js/listview_templates.js | 8 +- .../filegen/templates/global.js/mapviewer.js | 6 + .../tools/filegen/templates/global.js/menu.js | 2 +- .../templates/global.js/modelviewer.js | 6 +- .../templates/global.js/screenshots.js | 6 +- .../tools/filegen/templates/global.js/tabs.js | 7 +- .../filegen/templates/global.js/tracking.js | 317 +++++++++++------- .../filegen/templates/global.js/videos.js | 9 +- static/js/filters.js | 19 +- static/js/home.js | 6 +- template/bricks/head.tpl.php | 8 +- 14 files changed, 293 insertions(+), 166 deletions(-) create mode 100644 setup/sql/updates/1765569409_01.sql diff --git a/includes/components/pagetemplate.class.php b/includes/components/pagetemplate.class.php index a688114a..d05e0267 100644 --- a/includes/components/pagetemplate.class.php +++ b/includes/components/pagetemplate.class.php @@ -27,14 +27,14 @@ class PageTemplate private array $pageData = []; // processed by display hooks // template data that needs further processing .. ! WARNING ! they will not get aut fetched from $context as they are already defined here - private string $gStaticUrl; - private string $gHost; - private string $gServerTime; - private string $gUser; - private string $gFavorites; - private ?string $analyticsTag = null; - private bool $consentFooter = false; - private string $dbProfiles = ''; + private string $gStaticUrl; + private string $gHost; + private string $gServerTime; + private string $gUser; + private string $gFavorites; + private bool $hasAnalytics = false; + private bool $consentFooter = false; + private string $dbProfiles = ''; private readonly string $user; // becomes User object @@ -49,7 +49,7 @@ class PageTemplate $this->locale = Lang::getLocale(); $this->gStaticUrl = Cfg::get('STATIC_URL'); $this->gHost = Cfg::get('HOST_URL'); - $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); + $this->hasAnalytics = !!Cfg::get('GTAG_MEASUREMENT_ID'); $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); $this->user = User::class; } @@ -472,16 +472,16 @@ class PageTemplate private function update() : void { // analytics + consent - if ($this->analyticsTag && !isset($_COOKIE['consent'])) + if ($this->hasAnalytics && !isset($_COOKIE['consent'])) { $this->addScript(SC_CSS_FILE, 'css/consent.css'); $this->addScript(SC_JS_FILE, 'js/consent.js'); $this->consentFooter = true; - $this->analyticsTag = null; + $this->hasAnalytics = false; } - else if ($this->analyticsTag && !$_COOKIE['consent']) - $this->analyticsTag = null; + else if ($this->hasAnalytics && !$_COOKIE['consent']) + $this->hasAnalytics = false; // js + css $this->prepareScripts(); @@ -526,7 +526,7 @@ class PageTemplate { $this->gStaticUrl = Cfg::get('STATIC_URL'); $this->gHost = Cfg::get('HOST_URL'); - $this->analyticsTag = Cfg::get('GTAG_MEASUREMENT_ID'); + $this->hasAnalytics = !!Cfg::get('GTAG_MEASUREMENT_ID'); $this->gServerTime = sprintf("new Date('%s')", date(Util::$dateFormatInternal)); } diff --git a/setup/sql/updates/1765569409_01.sql b/setup/sql/updates/1765569409_01.sql new file mode 100644 index 00000000..03adf418 --- /dev/null +++ b/setup/sql/updates/1765569409_01.sql @@ -0,0 +1 @@ +UPDATE `aowow_dbversion` SET `build` = CONCAT(IFNULL(`build`, ''), ' globaljs'); diff --git a/setup/tools/filegen/templates/global.js/announcement.js b/setup/tools/filegen/templates/global.js/announcement.js index 0149b984..82628760 100644 --- a/setup/tools/filegen/templates/global.js/announcement.js +++ b/setup/tools/filegen/templates/global.js/announcement.js @@ -110,7 +110,12 @@ Announcement.prototype = { // aowow - animation fix - jQuery.animate hard snaps into place after half the time passed this.parentDiv.style.opacity = '100'; this.parentDiv.style.height = (this.parentDiv.offsetHeight + 10) + 'px'; - g_trackEvent('Announcements', 'Show', '' + this.name); + + $WH.Track.nonInteractiveEvent({ + category: 'Announcements', + action: 'Show', + label: '' + this.name + }); }, hide: function() @@ -132,7 +137,11 @@ Announcement.prototype = { markRead: function() { - g_trackEvent('Announcements', 'Close', '' + this.name); + $WH.Track.interactiveEvent({ + category: 'Announcements', + action: 'Close', + label: '' + this.name + }); g_setWowheadCookie('announcement-' + this.id, 'closed'); this.hide(); }, @@ -147,11 +156,22 @@ Announcement.prototype = { { this.text = text; Markup.printHtml(this.text, this.parent + '-markup'); - g_addAnalyticsToNode($WH.ge(this.parent + '-markup'), { - 'category': 'Announcements', - 'actions': { - 'Follow link': function(node) { return true; } - } - }, this.id); + + let parent = $WH.ge(this.parent + '-markup'); + $WH.qsa('a', parent).forEach(link => { + $WH.aE(link, 'click', () => { + let label = 'unknown'; + let txt = g_getFirstTextContent(link); + if (txt) + label = g_urlize(txt).substr(0, 80); + else if (link.title) + label = g_urlize(link.title).substr(0, 80); + else if (link.id) + label = g_urlize(link.id).substr(0, 80); + + label = `${ this.id || 0 }-${ label }`; + $WH.Track.linkClick(link, { category: 'Announcements', label: label }); + }); + }); } }; diff --git a/setup/tools/filegen/templates/global.js/listview_templates.js b/setup/tools/filegen/templates/global.js/listview_templates.js index 2c3dcb85..d05913fd 100644 --- a/setup/tools/filegen/templates/global.js/listview_templates.js +++ b/setup/tools/filegen/templates/global.js/listview_templates.js @@ -3296,7 +3296,7 @@ Listview.templates = { return Listview.funcBox.assocArrCmp(a.skill, b.skill, g_spell_skills); } }, - /* AoWoW: custom start */ + /* AoWoW: custom start */ { id: 'stackRules', name: LANG.asr_behaviour, @@ -3578,7 +3578,7 @@ Listview.templates = { return 0; } }, - /* AoWoW: custom end */ + /* AoWoW: custom end */ { id: 'completed', // Listview.COLUMN_ID_COMPLETION name: LANG.completion, // WH.TERMS.completion @@ -7350,7 +7350,7 @@ Listview.templates = { $(td).mouseover(function (event, menu) { $WH.Tooltip.showAtCursor(menu, event, 0, 0); }.bind(td, tt)); $(td).mousemove(function (event) { $WH.Tooltip.cursorUpdate(event); }) .mouseout(function () { $WH.Tooltip.hide(); }); -/* aowow - we dont do patches + /* aowow - we dont do patches var g = typeof g_hearthhead != "undefined" && g_hearthhead ? "hearthstone" : "wow"; if (!g_getPatchVersionObject.hasOwnProperty("parsed") || !g_getPatchVersionObject.parsed[g]) { g_getPatchVersionObject(); @@ -7369,7 +7369,7 @@ Listview.templates = { j = j.replace(f, f + " (" + new Date(c.timestamp).toDateString() + ")"); } } -*/ + */ let j = changelog.version; // aowow - tmp $(td).html(j); }, diff --git a/setup/tools/filegen/templates/global.js/mapviewer.js b/setup/tools/filegen/templates/global.js/mapviewer.js index 2f6ad712..59cec45b 100644 --- a/setup/tools/filegen/templates/global.js/mapviewer.js +++ b/setup/tools/filegen/templates/global.js/mapviewer.js @@ -276,6 +276,12 @@ var MapViewer = new function() this.show = function(opt) { + $WH.Track.interactiveEvent({ + category: "Zone Maps", + action: "Show", + label: opt.link ? opt.link : "General" + }); + if (opt.link) { tempParent = $WH.ce('div'); diff --git a/setup/tools/filegen/templates/global.js/menu.js b/setup/tools/filegen/templates/global.js/menu.js index c2416ae6..f65be754 100644 --- a/setup/tools/filegen/templates/global.js/menu.js +++ b/setup/tools/filegen/templates/global.js/menu.js @@ -792,7 +792,7 @@ var Menu = new function() }); explodeInto(menu, implodedMenu); - }; + } function explodeInto(menu, implodedMenu) // Reverse of implode { diff --git a/setup/tools/filegen/templates/global.js/modelviewer.js b/setup/tools/filegen/templates/global.js/modelviewer.js index 182dffa6..0c078614 100644 --- a/setup/tools/filegen/templates/global.js/modelviewer.js +++ b/setup/tools/filegen/templates/global.js/modelviewer.js @@ -604,7 +604,11 @@ var ModelViewer = new function() } } - g_trackEvent('Model Viewer', 'Show', g_urlize(trackCode)); + $WH.Track.interactiveEvent({ + category: 'Model Viewer', + action: 'Show', + label: g_urlize(trackCode) // WH.Strings.slug(trackCode) + }); oldHash = location.hash; } diff --git a/setup/tools/filegen/templates/global.js/screenshots.js b/setup/tools/filegen/templates/global.js/screenshots.js index 44ea902d..3a42f3c8 100644 --- a/setup/tools/filegen/templates/global.js/screenshots.js +++ b/setup/tools/filegen/templates/global.js/screenshots.js @@ -182,7 +182,11 @@ var ScreenshotViewer = new function() if (!resizing) { - g_trackEvent('Screenshots', 'Show', screenshot.id + ( (screenshot.caption && screenshot.caption.length) ? ' (' + screenshot.caption + ')' : '')); + $WH.Track.interactiveEvent({ + category: 'Screenshots', + action: 'Show', + label: screenshot.id + (screenshot.caption && screenshot.caption.length ? ` (${ screenshot.caption })` : '') + }); // ORIGINAL diff --git a/setup/tools/filegen/templates/global.js/tabs.js b/setup/tools/filegen/templates/global.js/tabs.js index 8f3406ca..c12767c0 100644 --- a/setup/tools/filegen/templates/global.js/tabs.js +++ b/setup/tools/filegen/templates/global.js/tabs.js @@ -359,6 +359,11 @@ Tabs.trackClick = function(tab) if (!this.trackable || tab.tracked) return; - g_trackEvent('Tabs', 'Show', this.trackable + ': ' + tab.id); + $WH.Track.interactiveEvent({ + category: 'Tab Click', + action: 'Page: ' + this.trackable, + label: 'Tab: ' + tab.id + }); + tab.tracked = 1; } diff --git a/setup/tools/filegen/templates/global.js/tracking.js b/setup/tools/filegen/templates/global.js/tracking.js index 6fe22256..a91901c6 100644 --- a/setup/tools/filegen/templates/global.js/tracking.js +++ b/setup/tools/filegen/templates/global.js/tracking.js @@ -1,130 +1,207 @@ -/* -TODO: Create "Tracking" class -*/ - -function g_trackPageview(tag) +// https://developers.google.com/tag-platform/security/guides/consent +$WH.Track = new function () { - function track() - { - if (typeof ga == 'function') - ga('send', 'pageview', tag); + const trackAction = 'Click'; + const siteVariables = { + // adsBlocked: 3, + // adsUnblocked: 4, + loggedInUserIsPremium: 2, + userIsLoggedIn: 1, + // userShouldSeeAds: 5 + }; + const maxRetryTime = 10000; + const retryTimeout = 10; + const scrollDepthPoints = [25, 50, 75, 90, 100]; + const _self = { + gaReady: false, + scriptAdded: undefined }; - $(document).ready(track); -} - -function g_trackEvent(category, action, label, value) -{ - function track() + this.gaInit = function (nTries) { - if (typeof ga == 'function') - ga('send', 'event', category, action, label, value); - }; - - $(document).ready(track); -} - -function g_attachTracking(node, category, action, label, value) -{ - var $node = $(node); - - $node.click(function() - { - g_trackEvent(category, action, label, value); - }); -} - -function g_addAnalytics() -{ - var objs = { - 'home-logo': { - 'category': 'Homepage Logo', - 'actions': { - 'Click image': function(node) { return true; } - } - }, - 'home-featuredbox': { - 'category': 'Featured Box', - 'actions': { - 'Follow link': function(node) { return (node.parentNode.className != 'home-featuredbox-links'); }, - 'Click image': function(node) { return (node.parentNode.className == 'home-featuredbox-links'); } - } - }, - 'home-oneliner': { - 'category': 'Oneliner', - 'actions': { - 'Follow link': function(node) { return true; } - } - }, - 'sidebar-container': { - 'category': 'Page sidebar', - 'actions': { - 'Click image': function(node) { return true; } - } - }, - 'toptabs-promo': { - 'category': 'Page header', - 'actions': { - 'Click image': function(node) { return true; } - } - } - }; - - for (var i in objs) - { - var e = $WH.ge(i); - if (e) - g_addAnalyticsToNode(e, objs[i]); - } -} - -function g_getNodeTextId(node) -{ - var id = null, - text = g_getFirstTextContent(node); - - if (text) - id = g_urlize(text); - else if (node.title) - id = g_urlize(node.title); - else if (node.id) - id = g_urlize(node.id); - - return id; -} - -function g_addAnalyticsToNode(node, opts, labelPrefix) -{ - if (!opts || !opts.actions || !opts.category) - { - if ($WH.isset('g_dev') && g_dev) + if (!_self.scriptAdded) { - console.log('Tried to add analytics event without appropriate parameters.'); - console.log(node); - console.log(opts); - } - - return; - } - - var category = opts.category; - var tags = $WH.gE(node, 'a'); - for (var i = 0; i < tags.length; ++i) - { - var node = tags[i]; - var action = 'Follow link'; - for (var a in opts.actions) - { - if (opts.actions[a] && opts.actions[a](node)) + (function (_window, _document, node, src, varName, gaJSNode, firstJSNode) { - action = a; - break; - } + _window['GoogleAnalyticsObject'] = varName; + _window[varName] = _window[varName] || function () { (_window[varName].q = _window[varName].q || []).push(arguments) }, + _window[varName].l = 1 * new Date; + + gaJSNode = _document.createElement(node), + firstJSNode = _document.getElementsByTagName(node)[0]; + gaJSNode.async = 1; + gaJSNode.src = src; + firstJSNode.parentNode.insertBefore(gaJSNode, firstJSNode); + })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); + + let attachGTAG = () => { + let script = document.createElement('script'); + script.async = true; + script.src = 'https://www.googletagmanager.com/gtag/js?id=CFG_GTAG_MEASUREMENT_ID'; + document.body.appendChild(script); + }; + + if (document.body) + attachGTAG(); + else + $WH.aE(document, 'DOMContentLoaded', attachGTAG); + + window.dataLayer = window.dataLayer || []; + window.gtag = function () { dataLayer.push(arguments) }; + + gtag('js', new Date); + gtag('config', 'CFG_GTAG_MEASUREMENT_ID'); + + _self.scriptAdded = true; } - var label = (labelPrefix ? labelPrefix + '-' : '') + g_getNodeTextId(node); - g_attachTracking(node, category, action, label); + if (!window.ga || !ga.create) + { + if (!nTries) + nTries = 1; + + if (nTries > 100) + return; + + setTimeout($WH.Track.gaInit.bind($WH.Track, nTries + 1), nTries * 9); + return; + } + + ga('create', 'UA_MEASUREMENT_KEY', 'CFG_UA_MEASUREMENT_KEY'); + // trackSiteVar(siteVariables.userShouldSeeAds, $WH.WAS.showAds()); + trackSiteVar(siteVariables.userIsLoggedIn, /* $WH.User.isLoggedIn() */g_user.id > 0); + // if ($WH.User.isLoggedIn()) + if (g_user.id > 0) + trackSiteVar(siteVariables.loggedInUserIsPremium, /* $WH.User.isPremium() */g_user.premium); + + ga('set', 'anonymizeIp', true); + ga('send', 'pageview'); + + _self.gaReady = true; + scrollDepthPoints.forEach(registerTrackScroll); + }; + + this.interactiveEvent = evt => trackEvent(evt ); + this.nonInteractiveEvent = evt => trackEvent(evt, { nonInteraction: true }); + this.interactiveEventOutgoing = evt => trackEvent(evt, { isOutgoing: true }); + this.linkClick = (anchor, evt) => trackEvent({ ...evt, action: trackAction, value: anchor.href }, { isOutgoing: true }); + + function trackSiteVar(idx, val) + { + ga('set', 'dimension' + idx, val) } -} -$(document).ready(g_addAnalytics); + function registerTrackScroll(depth) + { + let trackDone = false; + const trackScroll = () => { + if (trackDone) + return; + + trackDone = true; + requestAnimationFrame(() => { + const y = window.scrollY; + const h = document.documentElement.scrollHeight - document.documentElement.clientHeight; + if (y / h * 100 >= depth) + { + trackEvent({ + action: 'scroll_event', + event_category: 'Scroll Depth', + event_label: `${ depth }%`, + scroll_depth: depth + }); + + window.removeEventListener('scroll', trackScroll, { passive: true }) + } + trackDone = false; + }); + }; + + window.addEventListener('scroll', trackScroll, { passive: true }) + } + + function trackEvent(evt, opts) + { + const { action: act, ...o } = evt; + const { category: cat, label: lab, value: val } = o; + const { nonInteraction: ni, isOutgoing: io } = opts || {}; + let { retryCount: rc } = opts || {}; + + if (!_self.gaReady) + { + if ($WH.isset('g_dev') && g_dev) + return; + + if (!rc) + rc = 0; + + rc++; + if (rc * retryTimeout > maxRetryTime) + return; + + setTimeout(trackEvent.bind(null, evt, { + nonInteraction: ni, + isOutgoing: io, + retryCount: rc + }), retryTimeout); + + return; + } + + let attr; + if (typeof ni === 'boolean') + { + attr ??= {}; + attr.nonInteraction = ni ? 1 : 0; + } + + if (io) + { + attr ??= {}; + attr.transport = 'beacon'; + } + + if (cat) + ga('send', 'event', cat, act, lab, val, attr); + + gtag('event', act, o); + } +}; + +// aowow - repurpose old tracking +$(document).ready(function () { + var trackObjs = { + 'header-logo': { 'label': 'Database Logo', 'actions': { 'Click image': (node) => true } }, + 'home-logo': { 'label': 'Homepage Logo', 'actions': { 'Click image': (node) => true } }, + 'home-oneliner': { 'label': 'Oneliner', 'actions': { 'Follow link': (node) => true } }, + 'home-featuredbox': { 'label': 'Featured Box', 'actions': { 'Follow link': (node) => node.parentNode.className != 'home-featuredbox-links', + 'Click image': (node) => node.parentNode.className == 'home-featuredbox-links' } + } + }; + + Object.entries(trackObjs).forEach(([nodeId, trackInfo]) => { + let parent = $WH.ge(nodeId); + if (!parent) + return; + + $WH.qsa('a', parent).forEach(link => { + Object.entries(trackInfo.actions).forEach(([action, testFn]) => { + $WH.aE(link, 'click', evt => { + if (!testFn(link)) + return; + + let txt = 'unknown'; + if (_ = g_getFirstTextContent(link)) + txt = g_urlize(_).substr(0, 80); + else if (link.title) + txt = g_urlize(link.title).substr(0, 80); + else if (link.id) + txt = g_urlize(link.id).substr(0, 80); + + label = `${trackInfo.label}-${action}-${txt}`; + $WH.Track.linkClick(link, { category: PageTemplate.get('pageName') || 'unknown', label: label }); + }); + }); + }); + }); +}); diff --git a/setup/tools/filegen/templates/global.js/videos.js b/setup/tools/filegen/templates/global.js/videos.js index 5d8a858e..1e4664ae 100644 --- a/setup/tools/filegen/templates/global.js/videos.js +++ b/setup/tools/filegen/templates/global.js/videos.js @@ -176,7 +176,13 @@ var VideoViewer = new function() if (!resizing) { - g_trackEvent('Videos', 'Show', video.id + (video.caption.length ? ' (' + video.caption + ')' : '')); + var hasCaption = (video.caption != null && video.caption.length); + + $WH.Track.interactiveEvent({ + category: 'Videos', + action: 'Show', + label: video.id + (hasCaption ? ` (${ video.caption })` : '') + }); if (video.videoType == 1) imgDiv.innerHTML = Markup.toHtml('[youtube=' + video.videoId + ' width=' + imgWidth + ' height=' + imgHeight + ' autoplay=true]', {mode:Markup.MODE_ARTICLE}); @@ -249,7 +255,6 @@ var VideoViewer = new function() // CAPTION - var hasCaption = (video.caption != null && video.caption.length); var hasSubject = (video.subject != null && video.subject.length && video.type && video.typeId); if (hasCaption || hasSubject) diff --git a/static/js/filters.js b/static/js/filters.js index a09d5e6e..67c6e541 100644 --- a/static/js/filters.js +++ b/static/js/filters.js @@ -1088,15 +1088,20 @@ function fi_setCriteria(cr, crs, crv) { var i, - c = _.childNodes[0].childNodes[0]; + c = _.childNodes[0].childNodes[0], + s; _ = c.getElementsByTagName('option'); for (i = 0; i < _.length; ++i) { if (_[i].value == cr[0]) { _[i].selected = true; - if (fi_Lookup(cr[0])) { - g_trackEvent('Filters', fi_type, fi_Lookup(cr[0]).name); + if (s = fi_Lookup(cr[0])) { + $WH.Track.nonInteractiveEvent({ + category: "Filters", + action: fi_type, // vars.page, + label: s.name + }); } break; @@ -1108,8 +1113,12 @@ function fi_setCriteria(cr, crs, crv) { for (i = 1; i < cr.length && i < 5; ++i) { fi_criterionChange(fi_addCriterion(a, cr[i]), crs[i], crv[i]); - if (fi_Lookup(cr[i])) { - g_trackEvent('Filters', fi_type, fi_Lookup(cr[i]).name); + if (s = fi_Lookup(cr[i])) { + $WH.Track.nonInteractiveEvent({ + category: "Filters", + action: fi_type, // vars.page, + label: s.name + }); } } } diff --git a/static/js/home.js b/static/js/home.js index ae6969c3..6de5865f 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -16,7 +16,7 @@ $(document).ready(function () { $('.home-featuredbox-links a').hover( function () { $(this).next('var').addClass('active') }, function () { $(this).next('var').removeClass('active') } - ).click(function () { g_trackEvent('Featured Box', 'Click', this.title) } - ).each( function () { g_trackEvent('Featured Box', 'Impression', this.title) } - ) + ).click(function () { $WH.Track.interactiveEvent({category: 'Featured Box', action: 'Click', label: this.title}) } + ).each( function () { $WH.Track.nonInteractiveEvent({category: 'Featured Box', action: 'Show', label: this.title}) } + ); }); diff --git a/template/bricks/head.tpl.php b/template/bricks/head.tpl.php index 1f20cec2..2ecc6468 100644 --- a/template/bricks/head.tpl.php +++ b/template/bricks/head.tpl.php @@ -30,13 +30,9 @@ endif; ?> -analyticsTag): ?> - +hasAnalytics): ?>