From 369c2c62e498a3fc29be4ebc3fcbb9edae086bfb Mon Sep 17 00:00:00 2001 From: David Losada Carballo Date: Sat, 18 Jun 2022 18:02:15 +0200 Subject: [PATCH 1/3] feat(gtm): add custom user properties and events --- legacy/app/app.js | 3 +- legacy/app/auth/authentication.service.js | 10 +++ legacy/app/gtm-userprops.js | 74 +++++++++++++++++++++++ root/src/datalayer.js | 65 ++++++++++++++++++++ root/src/ushahidi-root-config.js | 2 + 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 legacy/app/gtm-userprops.js create mode 100644 root/src/datalayer.js diff --git a/legacy/app/app.js b/legacy/app/app.js index a503ffac98..3f14034178 100644 --- a/legacy/app/app.js +++ b/legacy/app/app.js @@ -253,4 +253,5 @@ angular event.title = title; window.dispatchEvent(event); }); - }]); + }]) + .run(require('./gtm-userprops.js')); diff --git a/legacy/app/auth/authentication.service.js b/legacy/app/auth/authentication.service.js index 11e3b1d428..5beffcb6a4 100644 --- a/legacy/app/auth/authentication.service.js +++ b/legacy/app/auth/authentication.service.js @@ -51,6 +51,16 @@ function ( language: userData.language }); loginStatus = true; + + // Refresh the user properties due to the change + window.dispatchEvent(new CustomEvent('ush:analytics:refreshUserProperties')); + window.dispatchEvent(new CustomEvent('datalayer:custom-event', { + detail: { + event: 'user logged in', + event_type: 'user_interaction', + user_role: userData.role == 'admin' ? 'admin' : 'member' + } + })); } function continueLogout(silent) { diff --git a/legacy/app/gtm-userprops.js b/legacy/app/gtm-userprops.js new file mode 100644 index 0000000000..28dd918a9d --- /dev/null +++ b/legacy/app/gtm-userprops.js @@ -0,0 +1,74 @@ +module.exports = [ + '$q', + 'ConfigEndpoint', + 'UserEndpoint', +function ($q, ConfigEndpoint, UserEndpoint) { + // Utility for collecting user properties from application state and pushing + // them to the datalayer + function getUserPropertiesPromise() { + var userProps = $q.defer(); + + var site = $q.defer(); + var multisite = $q.defer(); + var user = $q.defer(); + + ConfigEndpoint.get({id: 'site'}).$promise.then(function (x) { + site.resolve(x); + }); + ConfigEndpoint.get({id: 'multisite'}).$promise.then(function (x) { + multisite.resolve(x); + }); + UserEndpoint.get({id: 'me'}).$promise.then(function (x) { + if (x.id && x.role) { + // According to data dictionary: + // https://docs.google.com/document/d/1ulZerckIGun-mv4ASu2nEFGFTpqcYwBXd3HR95eKj68 + x.role_level = x.role == 'admin' ? 'admin' : 'member'; + } + user.resolve(x); + }).catch(function () { + user.resolve(null); + }) + + $q.all([site.promise, multisite.promise, user.promise]).then(function (results) { + var site = results[0] || {}; + var multisite = results[1] || {}; + var user = results[2] || {}; + + // This is a composition of the user id and deployment id, because each user must be unique + // in the analytics data warehouse + var scopedUserId = undefined; + if (user.id) { + scopedUserId = String(user.id) + "," + String(multisite.site_id || null); + } + + userProps.resolve({ + deployment_url: multisite.site_fqdn || window.location.host , + deployment_id: multisite.site_id || null, + deployment_name: site.name || undefined, + user_id: scopedUserId, + user_role: user.role_level || undefined, + browser_language: navigator.language + ? navigator.languages[0] + : (navigator.language || navigator.userLanguage) + }); + }); + + return userProps; + } + + function triggerRefresh() { + getUserPropertiesPromise().promise.then(function (userProps) { + let event = new CustomEvent('datalayer:userprops', { detail: userProps }); + window.dispatchEvent(event); + }); + } + + // Initialize user properties along with this module + triggerRefresh(); + + // Add event listener to refresh user properties on demand + window.addEventListener('ush:analytics:refreshUserProperties', function () { + triggerRefresh(); + }); + +}]; diff --git a/root/src/datalayer.js b/root/src/datalayer.js new file mode 100644 index 0000000000..02a5247923 --- /dev/null +++ b/root/src/datalayer.js @@ -0,0 +1,65 @@ +class Datalayer { + constructor() { + this.pushQ = []; + this.userProperties = {}; + } + + initialize() { + // Listen to user property change events + window.addEventListener("datalayer:userprops", ({detail}) => { + this.userProperties = detail; + if (this.pushQ.length > 0) { + for (var obj of this.pushQ) { + obj.user_properties = this.userProperties; + this.push(obj); + } + this.pushQ = []; + } else { + this.push({user_properties: this.userProperties}); + } + }); + + // Listen to custom events to be pushed + window.addEventListener("datalayer:custom-event", ({detail}) => { + this.push(detail); + }); + + // Watching for routing events + window.addEventListener("single-spa:routing-event", + ({ detail: { newUrl } }) => { + const path = new URL(newUrl).pathname; + const pageType = this.pathToPageType(path); + + // Corner case: routing event before user properties set + if (Object.keys(this.userProperties).length > 0) { + this.push({'page_type': pageType, user_properties: this.userProperties}); + } else { + // If user properties have not been initialised yet, we should queue + this.pushQ.push({'page_type': pageType}); + } + }); + } + + push(obj) { + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ ecommerce: null }); + window.dataLayer.push(obj); + console.log("pushed"); + console.log(obj); + } + + pathToPageType(path) { + // i.e. '/settings/general' -> ['settings', 'general'] + const tokens = path.split('/').filter(Boolean); + + if (tokens[0] == 'settings') { + return 'deployment-settings'; + } else if (tokens[0] == 'activity') { + return 'deployment-activity'; + } else { + return 'deployment-other'; + } + } +} + +export const datalayer = new Datalayer(); diff --git a/root/src/ushahidi-root-config.js b/root/src/ushahidi-root-config.js index 8ef4182307..37e2cc3665 100644 --- a/root/src/ushahidi-root-config.js +++ b/root/src/ushahidi-root-config.js @@ -6,6 +6,7 @@ import { } from "single-spa-layout"; import microfrontendLayout from "./microfrontend-layout.html"; import { getPageMetadata, setBootstrapConfig } from "@ushahidi/utilities"; +import { datalayer } from "./datalayer.js"; require("./loading.scss"); @@ -24,6 +25,7 @@ const layoutEngine = constructLayoutEngine({ routes, applications }); applications.forEach(registerApplication); layoutEngine.activate(); +datalayer.initialize(); const showError = function (show) { const element = document.querySelector('#bootstrap-error'); From 5b85d8e89872c82285493cba75173151fd7704b7 Mon Sep 17 00:00:00 2001 From: David Losada Carballo Date: Mon, 20 Jun 2022 09:03:24 +0200 Subject: [PATCH 2/3] fix: linting --- legacy/app/auth/authentication.service.js | 2 +- legacy/app/gtm-userprops.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/legacy/app/auth/authentication.service.js b/legacy/app/auth/authentication.service.js index 5beffcb6a4..df610cdb7f 100644 --- a/legacy/app/auth/authentication.service.js +++ b/legacy/app/auth/authentication.service.js @@ -58,7 +58,7 @@ function ( detail: { event: 'user logged in', event_type: 'user_interaction', - user_role: userData.role == 'admin' ? 'admin' : 'member' + user_role: userData.role === 'admin' ? 'admin' : 'member' } })); } diff --git a/legacy/app/gtm-userprops.js b/legacy/app/gtm-userprops.js index 28dd918a9d..afb30775e6 100644 --- a/legacy/app/gtm-userprops.js +++ b/legacy/app/gtm-userprops.js @@ -22,7 +22,7 @@ function ($q, ConfigEndpoint, UserEndpoint) { if (x.id && x.role) { // According to data dictionary: // https://docs.google.com/document/d/1ulZerckIGun-mv4ASu2nEFGFTpqcYwBXd3HR95eKj68 - x.role_level = x.role == 'admin' ? 'admin' : 'member'; + x.role_level = x.role === 'admin' ? 'admin' : 'member'; } user.resolve(x); }).catch(function () { @@ -38,7 +38,7 @@ function ($q, ConfigEndpoint, UserEndpoint) { // in the analytics data warehouse var scopedUserId = undefined; if (user.id) { - scopedUserId = String(user.id) + "," + String(multisite.site_id || null); + scopedUserId = String(user.id) + ',' + String(multisite.site_id || null); } userProps.resolve({ @@ -65,10 +65,10 @@ function ($q, ConfigEndpoint, UserEndpoint) { // Initialize user properties along with this module triggerRefresh(); - + // Add event listener to refresh user properties on demand window.addEventListener('ush:analytics:refreshUserProperties', function () { triggerRefresh(); }); - + }]; From c28c7c88fbf54cddc1721f9470b5112cfb8199bf Mon Sep 17 00:00:00 2001 From: David Losada Carballo Date: Wed, 22 Jun 2022 13:28:01 +0200 Subject: [PATCH 3/3] chore(docker): add GTM container id configuration via env variable --- app/config.js.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/config.js.j2 b/app/config.js.j2 index 12c7d85872..a7eebf3108 100644 --- a/app/config.js.j2 +++ b/app/config.js.j2 @@ -8,6 +8,9 @@ window.ushahidi = { gaKey: '{{ GA_KEY }}', {% else %} gaEnabled: false, +{% endif %} +{% if GTM_CONTAINER_ID %} + googleTagManager: '{{ GTM_CONTAINER_ID }}', {% endif %} intercomAppId: '{{ INTERCOM_APPID }}', appStoreId: '{{ APP_STORE_ID }}',