diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 989d57f0ed..597bf00b87 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -277,16 +277,6 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { next() }) - webRouter.use(function (req, res, next) { - res.locals.gaToken = - Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.token - res.locals.gaTokenV4 = - Settings.analytics && - Settings.analytics.ga && - Settings.analytics.ga.tokenV4 - next() - }) - webRouter.use(function (req, res, next) { res.locals.getReqQueryParam = field => req.query != null ? req.query[field] : undefined @@ -387,6 +377,15 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { sentryEnvironment: Settings.sentry.environment, sentryRelease: Settings.sentry.release, enableSubscriptions: Settings.enableSubscriptions, + gaToken: + Settings.analytics && + Settings.analytics.ga && + Settings.analytics.ga.token, + gaTokenV4: + Settings.analytics && + Settings.analytics.ga && + Settings.analytics.ga.tokenV4, + cookieDomain: Settings.cookieDomain, } next() }) diff --git a/services/web/app/views/_cookie_banner.pug b/services/web/app/views/_cookie_banner.pug new file mode 100644 index 0000000000..98f31b1510 --- /dev/null +++ b/services/web/app/views/_cookie_banner.pug @@ -0,0 +1,5 @@ +.cookie-banner.hidden-print.hidden + .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our cookie policy. + .cookie-banner-actions + button(type="button" class="btn btn-link btn-sm" data-ol-cookie-banner-set-consent="essential") Essential cookies only + button(type="button" class="btn btn-info btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies \ No newline at end of file diff --git a/services/web/app/views/_google_analytics.pug b/services/web/app/views/_google_analytics.pug new file mode 100644 index 0000000000..9b60144f97 --- /dev/null +++ b/services/web/app/views/_google_analytics.pug @@ -0,0 +1,50 @@ +if (typeof(ExposedSettings.gaTokenV4) != "undefined" || typeof(ExposedSettings.gaToken) != "undefined") + script(type="text/javascript", nonce=scriptNonce, id="ga-loader" data-ga-token=ExposedSettings.gaToken data-ga-token-v4=ExposedSettings.gaTokenV4 data-cookie-domain=ExposedSettings.cookieDomain). + var gaSettings = document.querySelector('#ga-loader').dataset; + var gaid = gaSettings.gaTokenV4; + var gaToken = gaSettings.gaToken; + var cookieDomain = gaSettings.cookieDomain; + if(gaid) { + window.dataLayer = window.dataLayer || []; + function gtag(){ + dataLayer.push(arguments); + } + gtag('js', new Date()); + gtag('config', gaid, { 'anonymize_ip': true }); + } + if (gaToken) { + window.ga = window.ga || function () { + (window.ga.q = window.ga.q || []).push(arguments); + }, window.ga.l = 1 * new Date(); + } + var loadGA = window.olLoadGA = function() { + if (gaid) { + var s = document.createElement('script'); + s.setAttribute('async', 'async'); + s.setAttribute('src', 'https://www.googletagmanager.com/gtag/js?id=' + gaid); + document.querySelector('head').append(s); + } + if (gaToken) { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + ga('create', gaToken, cookieDomain.replace(/^\./, "")); + ga('set', 'anonymizeIp', true); + ga('send', 'pageview'); + } + }; + // Check if consent given (features/cookie-banner) + var oaCookie = document.cookie.split('; ').find(function(cookie) { + return cookie.startsWith('oa='); + }); + if(oaCookie) { + var oaCookieValue = oaCookie.split('=')[1]; + if(oaCookieValue === '1') { + loadGA(); + } + } +else + script(type="text/javascript", nonce=scriptNonce). + window.ga = function() { console.log("would send to GA", arguments) }; + window.gtag = function() { console.log("would send to GA4", arguments) }; diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index b75855c922..bb9251e65a 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -25,43 +25,7 @@ html( link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) //- Scripts - - //- Google Analytics - if (typeof(gaToken) != "undefined") - script(type="text/javascript", nonce=scriptNonce). - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - script(type="text/javascript", nonce=scriptNonce). - ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}'); - ga('set', 'anonymizeIp', true); - ga('send', 'pageview'); - - try { - ga.isBlocked = localStorage.getItem('gaBlocked') === 'true' - if (!ga.isBlocked) { - window.addEventListener('load', function () { - setTimeout(function () { - if (!ga.loaded) localStorage.setItem('gaBlocked', 'true') - }, 4000) - }) - } - } catch (e) {} - else - script(type="text/javascript", nonce=scriptNonce). - window.ga = function() { console.log("would send to GA", arguments) }; - - if (typeof(gaTokenV4) != "undefined") - script(async, nonce=scriptNonce, src='https://www.googletagmanager.com/gtag/js?id=' + gaTokenV4) - script(type="text/javascript", nonce=scriptNonce). - window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', '#{gaTokenV4}'); - else - script(type = "text/javascript", nonce = scriptNonce). - window.gtag = function() { console.log("would send to GA4", arguments) }; + include _google_analytics block meta meta(name="ol-csrfToken" content=csrfToken) diff --git a/services/web/app/views/layout-marketing.pug b/services/web/app/views/layout-marketing.pug index ab5644af7c..3bfee2b4ae 100644 --- a/services/web/app/views/layout-marketing.pug +++ b/services/web/app/views/layout-marketing.pug @@ -17,4 +17,6 @@ block body else include layout/fat-footer + include _cookie_banner + != moduleIncludes("contactModal-marketing", locals) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 273be77860..26b3560436 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -15,4 +15,6 @@ block body else include layout/fat-footer + include _cookie_banner + != moduleIncludes("contactModal", locals) diff --git a/services/web/frontend/js/features/cookie-banner/index.js b/services/web/frontend/js/features/cookie-banner/index.js new file mode 100644 index 0000000000..1d562a7020 --- /dev/null +++ b/services/web/frontend/js/features/cookie-banner/index.js @@ -0,0 +1,43 @@ +function loadGA() { + if (window.olLoadGA) { + window.olLoadGA() + } +} + +function setConsent(value) { + document.querySelector('.cookie-banner').classList.add('hidden') + const cookieDomain = window.ExposedSettings.cookieDomain + const oneYearInSeconds = 60 * 60 * 24 * 365 + const cookieAttributes = + '; domain=' + + cookieDomain + + '; max-age=' + + oneYearInSeconds + + '; SameSite=Lax; Secure' + if (value === 'all') { + document.cookie = 'oa=1' + cookieAttributes + loadGA() + } else { + document.cookie = 'oa=0' + cookieAttributes + } +} + +if (window.ExposedSettings.gaToken || window.ExposedSettings.gaTokenV4) { + document + .querySelectorAll('[data-ol-cookie-banner-set-consent]') + .forEach(el => { + el.addEventListener('click', function (e) { + e.preventDefault() + const consentType = el.getAttribute('data-ol-cookie-banner-set-consent') + setConsent(consentType) + }) + }) + + const oaCookie = document.cookie.split('; ').find(c => c.startsWith('oa=')) + if (!oaCookie) { + const cookieBannerEl = document.querySelector('.cookie-banner') + if (cookieBannerEl) { + cookieBannerEl.classList.remove('hidden') + } + } +} diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js index 5dace05cea..6e5fe076b8 100644 --- a/services/web/frontend/js/main.js +++ b/services/web/frontend/js/main.js @@ -51,6 +51,7 @@ import './services/queued-http' import './services/validateCaptcha' import './services/validateCaptchaV3' import './filters/formatDate' +import './features/cookie-banner' import '../../modules/modules-main.js' import './cdn-load-test' angular.module('SharelatexApp').config(function ($locationProvider) { diff --git a/services/web/frontend/js/marketing.js b/services/web/frontend/js/marketing.js index 0e65f64da5..69ede04916 100644 --- a/services/web/frontend/js/marketing.js +++ b/services/web/frontend/js/marketing.js @@ -10,6 +10,7 @@ import './features/contact-form' import './features/event-tracking' import './features/fallback-image' import './features/multi-submit' +import './features/cookie-banner' $('[data-ol-lang-selector-tooltip]').tooltip({ trigger: 'hover' }) $('[data-toggle="tooltip"]').tooltip() diff --git a/services/web/frontend/stylesheets/components/footer.less b/services/web/frontend/stylesheets/components/footer.less index 1128dd3bd9..43a427b4eb 100644 --- a/services/web/frontend/stylesheets/components/footer.less +++ b/services/web/frontend/stylesheets/components/footer.less @@ -262,3 +262,37 @@ footer.site-footer { } } } + +.cookie-banner { + display: flex; + align-items: center; + flex-wrap: wrap; + padding: 10px 20px; + font-size: 0.9rem; + line-height: 1; + position: fixed; + bottom: 0px; + left: 0; + right: 0; + z-index: 100; + color: @text-color; + background: @ol-blue-gray-0; + box-shadow: 0 -2px 1px 0px #2c36455c; + + a { + color: @footer-link-color; + &:hover, + &:focus { + color: @footer-link-hover-color; + } + } +} + +.cookie-banner-content { + flex: 1; +} + +.cookie-banner-actions { + flex-shrink: 0; + white-space: nowrap; +}