[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
This commit is contained in:
Antoine Clausse
2025-06-30 09:58:08 +02:00
committed by Copybot
parent 735cc2272f
commit 00d5d879c5
13 changed files with 106 additions and 36 deletions
@@ -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,
@@ -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
)
+1 -1
View File
@@ -54,7 +54,7 @@ html(
)
//- Scripts
if typeof suppressGoogleAnalytics == 'undefined'
if typeof suppressAnalytics == 'undefined'
include _google_analytics
block meta
@@ -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
@@ -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'
@@ -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]')
@@ -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<boolean>).detail) {
loadHotjar()
}
})
}
@@ -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<boolean>).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)
}
@@ -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)
}
},
})
}
@@ -0,0 +1 @@
import './tracking-propensity'
+1
View File
@@ -25,6 +25,7 @@ export type ExposedSettings = {
isOverleaf: boolean
maxEntitiesPerProject: number
projectUploadTimeout: number
propensityId?: string
maxUploadSize: number
recaptchaDisabled: {
invite: boolean
+2
View File
@@ -25,5 +25,7 @@ declare global {
}
ga?: (...args: any) => void
gtag?: (...args: any) => void
propensity?: (propensityId?: string) => void
}
}
+1
View File
@@ -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"