Skip to content

Commit

Permalink
Merge pull request #1834 from SUI-Components/add-ga4-integration-object
Browse files Browse the repository at this point in the history
feat(packages/sui-segment-wrapper): add ga4 client id to segment events
  • Loading branch information
kikoruiz authored Oct 9, 2024
2 parents 6b843ae + 3eb7f2c commit ceb50c1
Show file tree
Hide file tree
Showing 17 changed files with 180 additions and 38 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/labeled_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/sui-segment-wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -159,6 +164,7 @@ Example:
```js
window.__mpi = {
segmentWrapper: {
googleAnalyticsMeasurementId: 'GA-123456789',
universalId: '7ab9ddf3281d5d5458a29e8b3ae2864',
defaultContext: {
site: 'comprocasa',
Expand Down
2 changes: 1 addition & 1 deletion packages/sui-segment-wrapper/src-umd/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 22 additions & 8 deletions packages/sui-segment-wrapper/src/index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
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)
window.analytics.addSourceMiddleware(userScreenInfo)
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'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {getConfig} from './config.js'
import {getConfig} from '../config.js'

let mcvid

Expand Down Expand Up @@ -53,7 +53,7 @@ export const getAdobeMarketingCloudVisitorIdFromWindow = () => {
}

const importVisitorApiAndGetAdobeMCVisitorID = () =>
import('./adobeVisitorApi.js').then(() => {
import('../scripts/adobeVisitorApi.js').then(() => {
mcvid = getAdobeMarketingCloudVisitorIdFromWindow()
return mcvid
})
Expand Down
54 changes: 54 additions & 0 deletions packages/sui-segment-wrapper/src/repositories/googleRepository.js
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
17 changes: 12 additions & 5 deletions packages/sui-segment-wrapper/src/segmentWrapper.js
Original file line number Diff line number Diff line change
@@ -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'}
Expand Down Expand Up @@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/sui-segment-wrapper/src/universalId.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
2 changes: 1 addition & 1 deletion packages/sui-segment-wrapper/test/checkAnonymousIdSpec.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
67 changes: 55 additions & 12 deletions packages/sui-segment-wrapper/test/segmentWrapperSpec.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,6 +15,7 @@ import {
resetReferrerState,
stubActualLocation,
stubDocumentCookie,
stubGoogleAnalytics,
stubReferrer,
stubTcfApi,
stubWindowObjects
Expand All @@ -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'
Expand Down Expand Up @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -569,7 +613,6 @@ describe('Segment Wrapper', function () {
integrations
}
}

const {traits} = spy.getCall(0).firstArg.obj.context

expect(context).to.deep.equal(expectation)
Expand Down
Loading

0 comments on commit ceb50c1

Please sign in to comment.