Przeglądaj źródła

[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
Antoine Clausse 1 rok temu
rodzic
commit
00d5d879c5

+ 1 - 0
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,

+ 10 - 0
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
+			)

+ 1 - 1
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

+ 1 - 1
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

+ 1 - 1
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'

+ 2 - 1
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]')

+ 7 - 32
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<boolean>).detail) {
-      loadHotjar()
-    }
-  })
 }

+ 55 - 0
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<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)
+}

+ 23 - 0
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)
+      }
+    },
+  })
+}

+ 1 - 0
services/web/frontend/js/infrastructure/tracking.ts

@@ -0,0 +1 @@
+import './tracking-propensity'

+ 1 - 0
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

+ 2 - 0
services/web/types/window.ts

@@ -25,5 +25,7 @@ declare global {
     }
     ga?: (...args: any) => void
     gtag?: (...args: any) => void
+
+    propensity?: (propensityId?: string) => void
   }
 }

+ 1 - 0
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"