diff --git a/.github/workflows/labeled_pull_request.yml b/.github/workflows/labeled_pull_request.yml index dddf37af8..0f7d1494a 100644 --- a/.github/workflows/labeled_pull_request.yml +++ b/.github/workflows/labeled_pull_request.yml @@ -28,7 +28,10 @@ jobs: cache: 'npm' - name: Install - run: npm install -D commander execa prettier + run: | + npm ci + npm install -D commander execa prettier + npx -y ultra-runner --raw --recursive prepublishOnly - name: Get Files id: files diff --git a/packages/sui-segment-wrapper/README.md b/packages/sui-segment-wrapper/README.md index 1a9ebd5e4..1425180ef 100644 --- a/packages/sui-segment-wrapper/README.md +++ b/packages/sui-segment-wrapper/README.md @@ -8,6 +8,11 @@ This package adds an abstraction layer on top of [segment.com](https://segment.c - [x] Send `user.id` and `anonymousId` on every track. - [x] Send anonymous data to AWS to be able to check data integrity with Adobe. +**Google Analytics 🔍** + +- [x] Load GA4 if `googleAnalyticsMeasurementId` is provided. +- [x] Retrieve `clientId` automatically from GA4 and put in Segment tracks. + **Adobe Marketing Cloud Visitor Id ☁️** - [x] Load _Adobe Visitor API_ when needed (if flag `importAdobeVisitorId` is set to `true`, otherwise you should load `Visitor API` by your own to get the `mcvid`). @@ -159,6 +164,7 @@ Example: ```js window.__mpi = { segmentWrapper: { + googleAnalyticsMeasurementId: 'GA-123456789', universalId: '7ab9ddf3281d5d5458a29e8b3ae2864', defaultContext: { site: 'comprocasa', diff --git a/packages/sui-segment-wrapper/src-umd/index.js b/packages/sui-segment-wrapper/src-umd/index.js index ca97f5cf6..1100863a7 100644 --- a/packages/sui-segment-wrapper/src-umd/index.js +++ b/packages/sui-segment-wrapper/src-umd/index.js @@ -1,4 +1,4 @@ -import {getAdobeMCVisitorID, getAdobeVisitorData} from '../src/adobeRepository.js' +import {getAdobeMCVisitorID, getAdobeVisitorData} from '../src/repositories/adobeRepository.js' import analytics from '../src/index.js' const w = window diff --git a/packages/sui-segment-wrapper/src/index.js b/packages/sui-segment-wrapper/src/index.js index 0d8f6bcc5..4139e5183 100644 --- a/packages/sui-segment-wrapper/src/index.js +++ b/packages/sui-segment-wrapper/src/index.js @@ -1,26 +1,27 @@ -import './patchAnalytics.js' +import './utils/patchAnalytics.js' import {defaultContextProperties} from './middlewares/source/defaultContextProperties.js' import {pageReferrer} from './middlewares/source/pageReferrer.js' import {userScreenInfo} from './middlewares/source/userScreenInfo.js' import {userTraits} from './middlewares/source/userTraits.js' -import {checkAnonymousId} from './checkAnonymousId.js' -import {isClient} from './config.js' +import {checkAnonymousId} from './utils/checkAnonymousId.js' +import {getConfig, isClient} from './config.js' import analytics from './segmentWrapper.js' import initTcfTracking from './tcf.js' import {getUserDataAndNotify} from './universalId.js' +import {loadGoogleAnalytics} from './repositories/googleRepository.js' -/* Initialize TCF Tracking with Segment */ +// Initialize TCF Tracking with Segment initTcfTracking() -/* Generate UniversalId if available */ +// Generate UniversalId if available try { getUserDataAndNotify() } catch (e) { console.error(`[segment-wrapper] UniversalID couldn't be initialized`) // eslint-disable-line } -/* Initialize middlewares */ +// Initialize middlewares const addMiddlewares = () => { window.analytics.addSourceMiddleware(userTraits) window.analytics.addSourceMiddleware(defaultContextProperties) @@ -28,12 +29,25 @@ const addMiddlewares = () => { window.analytics.addSourceMiddleware(pageReferrer) } -/* Initialize Segment on Client */ if (isClient && window.analytics) { + // Initialize Google Analtyics if needed + const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId') + + if (googleAnalyticsMeasurementId) { + window.dataLayer = window.dataLayer || [] + window.gtag = + window.gtag || + function gtag() { + window.dataLayer.push(arguments) + } + + loadGoogleAnalytics() + } + window.analytics.ready(checkAnonymousId) window.analytics.addSourceMiddleware ? addMiddlewares() : window.analytics.ready(addMiddlewares) } export default analytics -export {getAdobeVisitorData, getAdobeMCVisitorID} from './adobeRepository.js' +export {getAdobeVisitorData, getAdobeMCVisitorID} from './repositories/adobeRepository.js' export {getUniversalId} from './universalId.js' diff --git a/packages/sui-segment-wrapper/src/adobeRepository.js b/packages/sui-segment-wrapper/src/repositories/adobeRepository.js similarity index 95% rename from packages/sui-segment-wrapper/src/adobeRepository.js rename to packages/sui-segment-wrapper/src/repositories/adobeRepository.js index 801df3fe0..632b9f875 100644 --- a/packages/sui-segment-wrapper/src/adobeRepository.js +++ b/packages/sui-segment-wrapper/src/repositories/adobeRepository.js @@ -1,4 +1,4 @@ -import {getConfig} from './config.js' +import {getConfig} from '../config.js' let mcvid @@ -53,7 +53,7 @@ export const getAdobeMarketingCloudVisitorIdFromWindow = () => { } const importVisitorApiAndGetAdobeMCVisitorID = () => - import('./adobeVisitorApi.js').then(() => { + import('../scripts/adobeVisitorApi.js').then(() => { mcvid = getAdobeMarketingCloudVisitorIdFromWindow() return mcvid }) diff --git a/packages/sui-segment-wrapper/src/repositories/googleRepository.js b/packages/sui-segment-wrapper/src/repositories/googleRepository.js new file mode 100644 index 000000000..9c135b97d --- /dev/null +++ b/packages/sui-segment-wrapper/src/repositories/googleRepository.js @@ -0,0 +1,54 @@ +import {getConfig} from '../config.js' + +const FIELDS = { + clientId: 'client_id', + sessionId: 'session_id' +} + +const cachedData = { + [FIELDS.clientId]: null, + [FIELDS.sessionId]: null +} + +const loadScript = async src => + new Promise(function (resolve, reject) { + const script = document.createElement('script') + + script.src = src + script.onload = resolve + script.onerror = reject + document.head.appendChild(script) + }) + +export const loadGoogleAnalytics = async () => { + const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId') + + // Check we have the needed config to load the script + if (!googleAnalyticsMeasurementId) return Promise.resolve(false) + // Create the `gtag` script + const gtagScript = `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsMeasurementId}` + // Load it and retrieve the `clientId` from Google + return loadScript(gtagScript) +} + +const getGoogleField = async field => { + const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId') + + // If `googleAnalyticsMeasurementId` is not present, don't load anything + if (!googleAnalyticsMeasurementId) return Promise.resolve() + + return new Promise(resolve => { + // Try to get the field from the stored info + if (cachedData[field]) return resolve(cachedData[field]) + // if not, get it from the `GoogleAnalytics` tag + window.gtag?.('get', googleAnalyticsMeasurementId, field, id => { + // Cache locally the field value + cachedData[field] = id + // Resolve the promise with the field + resolve(id) + }) + }) +} + +export const getGoogleClientID = () => getGoogleField(FIELDS.clientId) +export const getGoogleSessionID = () => getGoogleField(FIELDS.sessionId) diff --git a/packages/sui-segment-wrapper/src/xandrRepository.js b/packages/sui-segment-wrapper/src/repositories/xandrRepository.js similarity index 89% rename from packages/sui-segment-wrapper/src/xandrRepository.js rename to packages/sui-segment-wrapper/src/repositories/xandrRepository.js index 2a17890dd..6cb1df0db 100644 --- a/packages/sui-segment-wrapper/src/xandrRepository.js +++ b/packages/sui-segment-wrapper/src/repositories/xandrRepository.js @@ -1,6 +1,6 @@ -import {readCookie, removeCookie, saveCookie} from './utils/cookies.js' -import {isClient, setConfig} from './config.js' -import {USER_GDPR} from './tcf.js' +import {readCookie, removeCookie, saveCookie} from '../utils/cookies.js' +import {isClient, setConfig} from '../config.js' +import {USER_GDPR} from '../tcf.js' const XANDR_ID_SERVER_URL = 'https://secure.adnxs.com/getuidj' const XANDR_ID_COOKIE = 'adit-xandr-id' diff --git a/packages/sui-segment-wrapper/src/adobeVisitorApi.js b/packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js similarity index 100% rename from packages/sui-segment-wrapper/src/adobeVisitorApi.js rename to packages/sui-segment-wrapper/src/scripts/adobeVisitorApi.js diff --git a/packages/sui-segment-wrapper/src/segmentWrapper.js b/packages/sui-segment-wrapper/src/segmentWrapper.js index 6bc86cdaf..6ee588282 100644 --- a/packages/sui-segment-wrapper/src/segmentWrapper.js +++ b/packages/sui-segment-wrapper/src/segmentWrapper.js @@ -1,9 +1,10 @@ // @ts-check -import {getAdobeMCVisitorID} from './adobeRepository.js' +import {getAdobeMCVisitorID} from './repositories/adobeRepository.js' +import {getGoogleClientID} from './repositories/googleRepository.js' import {getConfig} from './config.js' import {checkAnalyticsGdprIsAccepted, getGdprPrivacyValue} from './tcf.js' -import {getXandrId} from './xandrRepository.js' +import {getXandrId} from './repositories/xandrRepository.js' /* Default properties to be sent on all trackings */ const DEFAULT_PROPERTIES = {platform: 'web'} @@ -34,18 +35,24 @@ export const getDefaultProperties = () => ({ const getTrackIntegrations = async ({gdprPrivacyValue, event}) => { const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue) let marketingCloudVisitorId + let googleClientId if (isGdprAccepted) { marketingCloudVisitorId = await getAdobeMCVisitorID() + googleClientId = await getGoogleClientID() } const restOfIntegrations = getRestOfIntegrations({isGdprAccepted, event}) - const adobeAnalyticsIntegration = marketingCloudVisitorId ? {marketingCloudVisitorId} : true - // if we don't have the user consents we remove all the integrations but Adobe Analytics + // If we don't have the user consents we remove all the integrations but Adobe Analytics nor GA4 return { ...restOfIntegrations, - 'Adobe Analytics': adobeAnalyticsIntegration + 'Adobe Analytics': marketingCloudVisitorId ? {marketingCloudVisitorId} : true, + 'Google Analytics 4': googleClientId + ? { + clientId: googleClientId + } + : true } } diff --git a/packages/sui-segment-wrapper/src/universalId.js b/packages/sui-segment-wrapper/src/universalId.js index 3bc8a6f15..06c5e388e 100644 --- a/packages/sui-segment-wrapper/src/universalId.js +++ b/packages/sui-segment-wrapper/src/universalId.js @@ -2,6 +2,7 @@ import {dispatchEvent} from '@s-ui/js/lib/events' import {createUniversalId} from './utils/hashEmail.js' import {getConfig, isClient, setConfig} from './config.js' + const USER_DATA_READY_EVENT = 'USER_DATA_READY' export const getUniversalIdFromConfig = () => getConfig('universalId') diff --git a/packages/sui-segment-wrapper/src/checkAnonymousId.js b/packages/sui-segment-wrapper/src/utils/checkAnonymousId.js similarity index 100% rename from packages/sui-segment-wrapper/src/checkAnonymousId.js rename to packages/sui-segment-wrapper/src/utils/checkAnonymousId.js diff --git a/packages/sui-segment-wrapper/src/patchAnalytics.js b/packages/sui-segment-wrapper/src/utils/patchAnalytics.js similarity index 94% rename from packages/sui-segment-wrapper/src/patchAnalytics.js rename to packages/sui-segment-wrapper/src/utils/patchAnalytics.js index c7b37c6b7..6bd5fee99 100644 --- a/packages/sui-segment-wrapper/src/patchAnalytics.js +++ b/packages/sui-segment-wrapper/src/utils/patchAnalytics.js @@ -1,5 +1,5 @@ -import {isClient} from './config.js' -import {decorateContextWithNeededData, getDefaultProperties} from './segmentWrapper.js' +import {isClient} from '../config.js' +import {decorateContextWithNeededData, getDefaultProperties} from '../segmentWrapper.js' const MPI_PATCH_FIELD = '__mpi_patch' diff --git a/packages/sui-segment-wrapper/test/checkAnonymousIdSpec.js b/packages/sui-segment-wrapper/test/checkAnonymousIdSpec.js index dd8b3474b..4dafeceec 100644 --- a/packages/sui-segment-wrapper/test/checkAnonymousIdSpec.js +++ b/packages/sui-segment-wrapper/test/checkAnonymousIdSpec.js @@ -1,7 +1,7 @@ import {expect} from 'chai' import sinon from 'sinon' -import {checkAnonymousId} from '../src/checkAnonymousId.js' +import {checkAnonymousId} from '../src/utils/checkAnonymousId.js' describe('checkAnonymousId', () => { let anonymousId diff --git a/packages/sui-segment-wrapper/test/segmentWrapperSpec.js b/packages/sui-segment-wrapper/test/segmentWrapperSpec.js index 4f5ac6e1a..86a54025c 100644 --- a/packages/sui-segment-wrapper/test/segmentWrapperSpec.js +++ b/packages/sui-segment-wrapper/test/segmentWrapperSpec.js @@ -1,7 +1,7 @@ import {expect} from 'chai' import sinon from 'sinon' -import {getAdobeVisitorData} from '../src/adobeRepository.js' +import {getAdobeVisitorData} from '../src/repositories/adobeRepository.js' import {setConfig} from '../src/config.js' import suiAnalytics from '../src/index.js' import {defaultContextProperties} from '../src/middlewares/source/defaultContextProperties.js' @@ -15,6 +15,7 @@ import { resetReferrerState, stubActualLocation, stubDocumentCookie, + stubGoogleAnalytics, stubReferrer, stubTcfApi, stubWindowObjects @@ -37,6 +38,7 @@ describe('Segment Wrapper', function () { beforeEach(() => { stubWindowObjects() + stubGoogleAnalytics() window.__SEGMENT_WRAPPER = window.__SEGMENT_WRAPPER || {} window.__SEGMENT_WRAPPER.ADOBE_ORG_ID = '012345678@AdobeOrg' @@ -214,6 +216,53 @@ describe('Segment Wrapper', function () { }) }) + describe('and gtag has been configured properly', () => { + it('should send Google Analytics integration with true if user declined consents', async () => { + // Add the needed config to enable Google Analytics + setConfig('googleAnalyticsMeasurementId', 123) + await simulateUserDeclinedConsents() + + await suiAnalytics.track( + 'fakeEvent', + {}, + { + integrations: {fakeIntegrationKey: 'fakeIntegrationValue'} + } + ) + + const {context} = getDataFromLastTrack() + + expect(context.integrations).to.deep.includes({ + fakeIntegrationKey: 'fakeIntegrationValue', + 'Google Analytics 4': true + }) + }) + + it('should send ClientId on Google Analytics integration if user accepted consents', async () => { + // add needed config to enable Google Analytics + setConfig('googleAnalyticsMeasurementId', 123) + + await simulateUserAcceptConsents() + + await suiAnalytics.track( + 'fakeEvent', + {}, + { + integrations: {fakeIntegrationKey: 'fakeIntegrationValue'} + } + ) + + const {context} = getDataFromLastTrack() + + expect(context.integrations).to.deep.includes({ + fakeIntegrationKey: 'fakeIntegrationValue', + 'Google Analytics 4': { + clientId: 'fakeClientId' + } + }) + }) + }) + it('should add always the platform as web and the language', async () => { await suiAnalytics.track('fakeEvent', {fakePropKey: 'fakePropValue'}) const {properties} = getDataFromLastTrack() @@ -225,20 +274,20 @@ describe('Segment Wrapper', function () { }) it('should send defaultProperties if provided', async () => { - setConfig('defaultProperties', {site: 'midudev', vertical: 'blog'}) + setConfig('defaultProperties', {site: 'mysite', vertical: 'myvertical'}) await suiAnalytics.track('fakeEvent') const {properties} = getDataFromLastTrack() expect(properties).to.deep.equal({ - site: 'midudev', - vertical: 'blog', + site: 'mysite', + vertical: 'myvertical', platform: 'web' }) }) - describe('TCF is handled', () => { + describe('and the TCF is handled', () => { it('should reset the anonymousId when the user first declines and then accepts', async () => { await simulateUserDeclinedConsents() @@ -530,16 +579,12 @@ describe('Segment Wrapper', function () { const spy = sinon.stub() await simulateUserDeclinedConsents() - await suiAnalytics.track( 'fakeEvent', {fakePropKey: 'fakePropValue'}, { anonymousId: '1a3bfbfc-9a89-437a-8f1c-87d786f2b6a', userId: 'fakeId', - integrations: { - 'Midu Analytics': true - }, protocols: { event_version: 3 } @@ -549,14 +594,13 @@ describe('Segment Wrapper', function () { const {context} = getDataFromLastTrack() const integrations = { - 'Midu Analytics': true, All: false, 'Adobe Analytics': true, + 'Google Analytics 4': true, Personas: false, Webhooks: true, Webhook: true } - const expectation = { anonymousId: '1a3bfbfc-9a89-437a-8f1c-87d786f2b6a', integrations, @@ -569,7 +613,6 @@ describe('Segment Wrapper', function () { integrations } } - const {traits} = spy.getCall(0).firstArg.obj.context expect(context).to.deep.equal(expectation) diff --git a/packages/sui-segment-wrapper/test/stubs.js b/packages/sui-segment-wrapper/test/stubs.js index 0d6d88b7c..13b1738d3 100644 --- a/packages/sui-segment-wrapper/test/stubs.js +++ b/packages/sui-segment-wrapper/test/stubs.js @@ -10,6 +10,7 @@ export const cleanWindowStubs = () => { delete window.__borosTcf delete window.__mpi delete window.__tcfapi + delete window.gtag } export const stubTcfApi = ({success = true, eventStatus = 'cmpuishown', consents = {}} = {}) => { @@ -41,6 +42,19 @@ export const stubFetch = ({responses = [{urlRe: /^http/, fetchResponse: {}}]} = }) } +export const stubGoogleAnalytics = () => { + const mockClientId = 'fakeClientId' + const mockSessionId = 'fakeSessionId' + + window.gtag = (key, id, fieldName, done) => { + if (key === 'get') { + const value = fieldName === 'client_id' ? mockClientId : mockSessionId + + return done(value) + } + } +} + export const stubWindowObjects = ({borosMock = true, borosSuccess = true, isDmpAccepted = true} = {}) => { stubTcfApi() diff --git a/packages/sui-segment-wrapper/test/universalIdSpec.js b/packages/sui-segment-wrapper/test/universalIdSpec.js index c95a224cf..b4fbee5ea 100644 --- a/packages/sui-segment-wrapper/test/universalIdSpec.js +++ b/packages/sui-segment-wrapper/test/universalIdSpec.js @@ -4,8 +4,8 @@ import {getConfig, setConfig} from '../src/config.js' import {getUniversalId, getUserDataAndNotify} from '../src/universalId.js' import {cleanWindowStubs} from './stubs.js' -const UNIVERSAL_ID_EXAMPLE = '7ab9ddf3281d5d5458a29e8b3ae2864335087f1272d41ba440bee23d6acb911b' -const USER_EMAIL_EXAMPLE = 'miduga@gmail.com' +const UNIVERSAL_ID_EXAMPLE = '043d36c36dad0741bdebce605d0ee4e6c1dea6e2eb6399864dec7a59432a20c4' +const USER_EMAIL_EXAMPLE = 'test@sui.com' const USER_DATA_READY_EVENT = 'USER_DATA_READY' describe('Universal Id', () => { diff --git a/packages/sui-segment-wrapper/test/xandrRepositorySpec.js b/packages/sui-segment-wrapper/test/xandrRepositorySpec.js index b6271935f..40c6ae53f 100644 --- a/packages/sui-segment-wrapper/test/xandrRepositorySpec.js +++ b/packages/sui-segment-wrapper/test/xandrRepositorySpec.js @@ -1,7 +1,7 @@ import {expect} from 'chai' import {USER_GDPR} from '../src/tcf.js' -import {getXandrId} from '../src/xandrRepository.js' +import {getXandrId} from '../src/repositories/xandrRepository.js' import {stubDocumentCookie, stubFetch} from './stubs.js' import {waitUntil} from './utils.js'