From 00d5d879c59b31f5baf3b2636296ed7a978cd6ea Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 30 Jun 2025 09:58:08 +0200 Subject: [PATCH] [web] Add third-party tracking Propensity (#26638) * Rename `suppressGoogleAnalytics` to `suppressAnalytics` * Add Propensity script * Add LinkedIn Insight Tag script * Version the cookie to prevent adding unconsented tracking * Move tracking loaders to Typescript, insert them in foot_scripts.pug * Show the cookie-banner when tracking other than GA is set * Revert `oa` cookie versioning * Remove `async` from propensity script * Use shared tracking loader for Hotjar, LinkedIn, and Propensity * Reusable `insertScript` * Remove tracking-linkedin * Test the scripts by adding fake ids * Revert "Test the scripts by adding fake ids" This reverts commit 50759bb6b40fd2684d1b967d83dd71e8517c3de9. GitOrigin-RevId: 2a7b36bfc70ac1fc983f837dd4693a19a385cbc6 --- .../app/src/infrastructure/ExpressLocals.js | 1 + .../web/app/views/_mixins/foot_scripts.pug | 10 ++++ services/web/app/views/layout-base.pug | 2 +- .../project/editor/socket_diagnostics.pug | 2 +- .../app/views/user/compromised_password.pug | 2 +- .../js/features/cookie-banner/index.js | 3 +- .../web/frontend/js/infrastructure/hotjar.ts | 39 +++---------- .../js/infrastructure/tracking-loader.ts | 55 +++++++++++++++++++ .../js/infrastructure/tracking-propensity.ts | 23 ++++++++ .../frontend/js/infrastructure/tracking.ts | 1 + services/web/types/exposed-settings.ts | 1 + services/web/types/window.ts | 2 + services/web/webpack.config.js | 1 + 13 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 services/web/frontend/js/infrastructure/tracking-loader.ts create mode 100644 services/web/frontend/js/infrastructure/tracking-propensity.ts create mode 100644 services/web/frontend/js/infrastructure/tracking.ts diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 34eda0ba2d..51c2c2186b 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -420,6 +420,7 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.tokenV4, + propensityId: Settings?.analytics?.propensity?.id, cookieDomain: Settings.cookieDomain, templateLinks: Settings.templateLinks, labsEnabled: Settings.labs && Settings.labs.enable, diff --git a/services/web/app/views/_mixins/foot_scripts.pug b/services/web/app/views/_mixins/foot_scripts.pug index 717c46cdd9..07e154663e 100644 --- a/services/web/app/views/_mixins/foot_scripts.pug +++ b/services/web/app/views/_mixins/foot_scripts.pug @@ -1,6 +1,7 @@ mixin foot-scripts each file in entrypointScripts(entrypoint) script(type='text/javascript' nonce=scriptNonce src=file defer=deferScripts) + if settings.devToolbar.enabled each file in entrypointScripts('devToolbar') script( @@ -9,3 +10,12 @@ mixin foot-scripts src=file defer=deferScripts ) + + if typeof suppressAnalytics == 'undefined' + each file in entrypointScripts('tracking') + script( + type='text/javascript' + nonce=scriptNonce + src=file + defer=deferScripts + ) diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index b590618387..99fd4244f5 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -54,7 +54,7 @@ html( ) //- Scripts - if typeof suppressGoogleAnalytics == 'undefined' + if typeof suppressAnalytics == 'undefined' include _google_analytics block meta diff --git a/services/web/app/views/project/editor/socket_diagnostics.pug b/services/web/app/views/project/editor/socket_diagnostics.pug index b288361e33..31bf763b65 100644 --- a/services/web/app/views/project/editor/socket_diagnostics.pug +++ b/services/web/app/views/project/editor/socket_diagnostics.pug @@ -3,7 +3,7 @@ extends ../../layout-react block vars - var suppressNavbar = true - var suppressFooter = true - - var suppressGoogleAnalytics = true + - var suppressAnalytics = true - isWebsiteRedesign = 'true' block entrypointVar diff --git a/services/web/app/views/user/compromised_password.pug b/services/web/app/views/user/compromised_password.pug index 48017b0ea7..ccf32fc757 100644 --- a/services/web/app/views/user/compromised_password.pug +++ b/services/web/app/views/user/compromised_password.pug @@ -3,7 +3,7 @@ extends ../layout-react block vars - var suppressNavbar = true - var suppressFooter = true - - var suppressGoogleAnalytics = true + - var suppressAnalytics = true block entrypointVar - entrypoint = 'pages/compromised-password' diff --git a/services/web/frontend/js/features/cookie-banner/index.js b/services/web/frontend/js/features/cookie-banner/index.js index 20f62db8a5..4acb2a6a6c 100644 --- a/services/web/frontend/js/features/cookie-banner/index.js +++ b/services/web/frontend/js/features/cookie-banner/index.js @@ -29,7 +29,8 @@ function setConsent(value) { if ( getMeta('ol-ExposedSettings').gaToken || - getMeta('ol-ExposedSettings').gaTokenV4 + getMeta('ol-ExposedSettings').gaTokenV4 || + getMeta('ol-ExposedSettings').propensityId ) { document .querySelectorAll('[data-ol-cookie-banner-set-consent]') diff --git a/services/web/frontend/js/infrastructure/hotjar.ts b/services/web/frontend/js/infrastructure/hotjar.ts index 67051d088f..ea660e7faf 100644 --- a/services/web/frontend/js/infrastructure/hotjar.ts +++ b/services/web/frontend/js/infrastructure/hotjar.ts @@ -1,43 +1,18 @@ import getMeta from '@/utils/meta' import { debugConsole } from '@/utils/debugging' import { initializeHotjar } from '@/infrastructure/hotjar-snippet' +import { createTrackingLoader } from '@/infrastructure/tracking-loader' const { hotjarId, hotjarVersion } = getMeta('ol-ExposedSettings') const shouldLoadHotjar = getMeta('ol-shouldLoadHotjar') -let hotjarInitialized = false - if (hotjarId && hotjarVersion && shouldLoadHotjar) { - const loadHotjar = () => { - // consent needed - if (!document.cookie.split('; ').some(item => item === 'oa=1')) { - return - } - - if (!/^\d+$/.test(hotjarId) || !/^\d+$/.test(hotjarVersion)) { - debugConsole.error('Invalid Hotjar id or version') - return - } - - // avoid inserting twice - if (!hotjarInitialized) { - debugConsole.log('Loading Hotjar') - hotjarInitialized = true - initializeHotjar(hotjarId, hotjarVersion) - } - } - - // load when idle, if supported - if (typeof window.requestIdleCallback === 'function') { - window.requestIdleCallback(loadHotjar) + if (!/^\d+$/.test(hotjarId) || !/^\d+$/.test(hotjarVersion)) { + debugConsole.error('Invalid Hotjar id or version') } else { - loadHotjar() + createTrackingLoader( + () => initializeHotjar(hotjarId, hotjarVersion), + 'Hotjar' + ) } - - // listen for consent - window.addEventListener('cookie-consent', event => { - if ((event as CustomEvent).detail) { - loadHotjar() - } - }) } diff --git a/services/web/frontend/js/infrastructure/tracking-loader.ts b/services/web/frontend/js/infrastructure/tracking-loader.ts new file mode 100644 index 0000000000..c179fc2f86 --- /dev/null +++ b/services/web/frontend/js/infrastructure/tracking-loader.ts @@ -0,0 +1,55 @@ +import { debugConsole } from '@/utils/debugging' + +export const createTrackingLoader = (cb: () => void, name: string) => { + // avoid inserting twice + let initialized = false + + const loadTracking = () => { + // consent needed + const consent = document.cookie.split('; ').some(item => item === 'oa=1') + if (initialized || !consent) { + return + } + debugConsole.log('Loading Analytics', name) + initialized = true + cb() + } + + // load when idle, if supported + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(loadTracking) + } else { + loadTracking() + } + + // listen for consent + window.addEventListener('cookie-consent', event => { + if ((event as CustomEvent).detail) { + loadTracking() + } + }) +} + +export const insertScript = (attr: { + src: string + crossorigin?: string + async?: boolean + onload?: () => void +}) => { + const script = document.createElement('script') + script.setAttribute('src', attr.src) + + if (attr.crossorigin) { + script.setAttribute('crossorigin', attr.crossorigin) + } + + if (attr.async) { + script.setAttribute('async', 'async') + } + + if (attr.onload) { + script.onload = attr.onload + } + + document.querySelector('head')?.append(script) +} diff --git a/services/web/frontend/js/infrastructure/tracking-propensity.ts b/services/web/frontend/js/infrastructure/tracking-propensity.ts new file mode 100644 index 0000000000..0392995140 --- /dev/null +++ b/services/web/frontend/js/infrastructure/tracking-propensity.ts @@ -0,0 +1,23 @@ +import getMeta from '@/utils/meta' +import { + createTrackingLoader, + insertScript, +} from '@/infrastructure/tracking-loader' + +const { propensityId } = getMeta('ol-ExposedSettings') + +if (propensityId) { + createTrackingLoader(() => loadPropensityScript(propensityId), 'Propensity') +} + +const loadPropensityScript = (id: string) => { + insertScript({ + src: 'https://cdn.propensity.com/propensity/propensity_analytics.js', + crossorigin: 'anonymous', + onload: () => { + if (typeof window.propensity !== 'undefined') { + window.propensity(id) + } + }, + }) +} diff --git a/services/web/frontend/js/infrastructure/tracking.ts b/services/web/frontend/js/infrastructure/tracking.ts new file mode 100644 index 0000000000..ce26bd51a1 --- /dev/null +++ b/services/web/frontend/js/infrastructure/tracking.ts @@ -0,0 +1 @@ +import './tracking-propensity' diff --git a/services/web/types/exposed-settings.ts b/services/web/types/exposed-settings.ts index 9656441fa9..f9d929d13a 100644 --- a/services/web/types/exposed-settings.ts +++ b/services/web/types/exposed-settings.ts @@ -25,6 +25,7 @@ export type ExposedSettings = { isOverleaf: boolean maxEntitiesPerProject: number projectUploadTimeout: number + propensityId?: string maxUploadSize: number recaptchaDisabled: { invite: boolean diff --git a/services/web/types/window.ts b/services/web/types/window.ts index d2856e7179..5688faa9a4 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -25,5 +25,7 @@ declare global { } ga?: (...args: any) => void gtag?: (...args: any) => void + + propensity?: (propensityId?: string) => void } } diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 4b8ec18113..94144eeea2 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -26,6 +26,7 @@ const entryPoints = { 'main-light-style': './frontend/stylesheets/main-light-style.less', 'main-style-bootstrap-5': './frontend/stylesheets/bootstrap-5/main-style.scss', + tracking: './frontend/js/infrastructure/tracking.ts', } // Add entrypoints for each "page"