Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions packages/sui-segment-wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ This package adds an abstraction layer on top of [segment.com](https://segment.c
- [x] Add `page` method that internally uses `track` but with the correct `referrer` property.
- [x] Send `user.id` and `anonymousId` on every track.

**Google Analytics 🔍**

- [x] Load GA4 if `googleAnalyticsMeasurementId` is provided.
- [x] Retrieve `clientId` and `sessionId` automatically from GA4 and put in Segment tracks.

**Consent Management Platform 🐾**

- [x] Automatic tracking of Consent Management Platform usage.
Expand Down Expand Up @@ -124,9 +119,6 @@ import analytics from '@s-ui/segment-wrapper'

You could put a special config in a the `window.__mpi` to change some behaviour of the wrapper. This config MUST somewhere before using the Segment Wrapper.

- `googleAnalyticsMeasurementId`: _(optional)_ If set, this value will be used for the Google Analytics Measurement API. It will load `gtag` to get the client id.
- `googleAnalyticsConfig`: _(optional)_ If set, this config will be passed when initializing the Google Analytics Measurement API.
- `googleAnalyticsInitEvent`: _(optional)_ If set, an event will be sent in order to initialize all the Google Analytics data.
- `defaultContext`: _(optional)_ If set, properties will be merged and sent with every `track` and `page` in the **context object**. It's the ideal place to put the `site` and `vertical` info to make sure that static info will be sent along with all the tracking.
- `defaultProperties`: _(optional)_ If set, properties will be merged and sent with every `track` and `page`.
- `tcfTrackDefaultProperties` _(optional)_ If set, this property will be merged together with the default properties set to send with every tcf track event
Expand All @@ -141,7 +133,6 @@ Example:
```js
window.__mpi = {
segmentWrapper: {
googleAnalyticsMeasurementId: 'GA-123456789',
universalId: '7ab9ddf3281d5d5458a29e8b3ae2864',
defaultContext: {
site: 'comprocasa',
Expand Down
59 changes: 35 additions & 24 deletions packages/sui-segment-wrapper/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
sendGoogleConsents
} from './repositories/googleRepository.js'
import {checkAnonymousId} from './utils/checkAnonymousId.js'
import {isGA4DestinationEnabled} from './utils/ga4Detection.js'
import {getConfig, isClient} from './config.js'
import analytics from './segmentWrapper.js'
import initTcfTracking from './tcf.js'
Expand Down Expand Up @@ -44,33 +45,43 @@ const addMiddlewares = () => {
}

if (isClient && window.analytics) {
// Initialize Google Analtyics if needed
const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
const dataLayerName = getConfig('googleAnalyticsDataLayer') || DEFAULT_DATA_LAYER_NAME
const needsConsentManagement = getConfig('googleAnalyticsConsentManagement')
// Pre-cache GA4 destination detection as early as possible
// This ensures the first track has the correct integration mode
window.analytics.ready(() => {
// Force detection to cache the result
const useSegmentGA4Destination = isGA4DestinationEnabled()

if (googleAnalyticsMeasurementId) {
const googleAnalyticsConfig = getConfig('googleAnalyticsConfig')
// Only initialize manual GA4 if destination is not enabled (legacy mode)
if (!useSegmentGA4Destination) {
// Legacy behavior: Initialize Google Analytics manually
const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
const dataLayerName = getConfig('googleAnalyticsDataLayer') || DEFAULT_DATA_LAYER_NAME
const needsConsentManagement = getConfig('googleAnalyticsConsentManagement')

window[dataLayerName] = window[dataLayerName] || []
window.gtag =
window.gtag ||
function gtag() {
window[dataLayerName].push(arguments)
}
if (googleAnalyticsMeasurementId) {
const googleAnalyticsConfig = getConfig('googleAnalyticsConfig')

window.gtag('js', new Date())
if (needsConsentManagement) sendGoogleConsents()
window.gtag('config', googleAnalyticsMeasurementId, {
cookie_prefix: 'segment',
send_page_view: false,
...googleAnalyticsConfig,
...getCampaignDetails()
})
loadGoogleAnalytics().catch(error => {
console.error(error)
})
}
window[dataLayerName] = window[dataLayerName] || []
window.gtag =
window.gtag ||
function gtag() {
window[dataLayerName].push(arguments)
}

window.gtag('js', new Date())
if (needsConsentManagement) sendGoogleConsents()
window.gtag('config', googleAnalyticsMeasurementId, {
cookie_prefix: 'segment',
send_page_view: false,
...googleAnalyticsConfig,
...getCampaignDetails()
})
loadGoogleAnalytics().catch(error => {
console.error(error)
})
}
}
})

window.analytics.ready(checkAnonymousId)
window.analytics.addSourceMiddleware ? addMiddlewares() : window.analytics.ready(addMiddlewares)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import {getConfig} from '../../config.js'
import {getCampaignDetails} from '../../repositories/googleRepository.js'

export const campaignContext = ({payload, next}) => {
const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
const campaignDetails = getCampaignDetails({needsTransformation: false})

if (googleAnalyticsMeasurementId) {
const campaignDetails = getCampaignDetails({needsTransformation: false})

payload.obj.context = {
...payload.obj.context,
...campaignDetails
}
payload.obj.context = {
...payload.obj.context,
...campaignDetails
}

next(payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {dispatchEvent} from '@s-ui/js/lib/events'
import {getConfig} from '../config.js'
import {EVENTS} from '../events.js'
import {utils} from '../middlewares/source/pageReferrer.js'
import {isGA4DestinationEnabled} from '../utils/ga4Detection.js'

const FIELDS = {
clientId: 'client_id',
Expand Down Expand Up @@ -72,7 +73,11 @@ export const loadGoogleAnalytics = async () => {
}

// Trigger GA init event just once per session.
// Only in LEGACY mode (manual GA4 initialization)
const triggerGoogleAnalyticsInitEvent = sessionId => {
// Don't trigger if using Segment destination (new mode)
if (isGA4DestinationEnabled()) return

const eventName = getConfig('googleAnalyticsInitEvent') ?? DEFAULT_GA_INIT_EVENT
const eventPrefix = `ga_event_${eventName}_`
const eventKey = `${eventPrefix}${sessionId}`
Expand Down
99 changes: 73 additions & 26 deletions packages/sui-segment-wrapper/src/segmentWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
import {
CONSENT_STATES,
getConsentState,
getGoogleConsentValue,
getGoogleClientId,
getGoogleConsentValue,
getGoogleSessionId,
setGoogleUserId,
sendGoogleConsents
sendGoogleConsents,
setGoogleUserId
} from './repositories/googleRepository.js'
import {getXandrId} from './repositories/xandrRepository.js'
import {getConfig} from './config.js'
import {USER_GDPR, CMP_TRACK_EVENT, checkAnalyticsGdprIsAccepted, getGdprPrivacyValue} from './tcf.js'
import {isGA4DestinationEnabled} from './utils/ga4Detection.js'

/* Default properties to be sent on all trackings */
const DEFAULT_PROPERTIES = {platform: 'web'}
Expand Down Expand Up @@ -54,6 +55,20 @@ export const getDefaultProperties = () => ({
*/
const getTrackIntegrations = async ({gdprPrivacyValue, event}) => {
const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue)
const restOfIntegrations = getRestOfIntegrations({isGdprAccepted, event})

// Check if we should use manual GA4 (legacy) or Segment destination (new)
const useSegmentGA4Destination = isGA4DestinationEnabled()

if (useSegmentGA4Destination) {
// New behavior: let Segment handle GA4
return {
...restOfIntegrations,
'Google Analytics 4 Web': true
}
}

// Legacy behavior: manual GA4 with clientId/sessionId
let sessionId
let clientId

Expand All @@ -67,9 +82,6 @@ const getTrackIntegrations = async ({gdprPrivacyValue, event}) => {
)
}

const restOfIntegrations = getRestOfIntegrations({isGdprAccepted, event})

// If we don't have the user consents we remove all the integrations but GA4
return {
...restOfIntegrations,
'Google Analytics 4':
Expand Down Expand Up @@ -142,19 +154,18 @@ const getConsentValue = gdprValue => (gdprValue === USER_GDPR.ACCEPTED ? CONSENT
* @param {object} context Context object with all the actual info
* @returns {Promise<object>} New context with all the previous info and the new one
*/
export const decorateContextWithNeededData = async ({event = '', context = {}}) => {
export const decorateContextWithNeededData = async ({event = '', context = {}, properties = {}}) => {
const gdprPrivacyValue = await getGdprPrivacyValue()
const {analytics: gdprPrivacyValueAnalytics, advertising: gdprPrivacyValueAdvertising} = gdprPrivacyValue || {}
const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue)

// Check if we should use Segment destination or legacy mode
const useSegmentGA4Destination = isGA4DestinationEnabled()

const [integrations, xandrId] = await Promise.all([
getTrackIntegrations({gdprPrivacyValue, event}),
getXandrId({gdprPrivacyValueAdvertising})
])
const analyticsConsentValue = getGoogleConsentValue('analytics_storage') ?? getConsentValue(gdprPrivacyValueAnalytics)
const adUserDataConsentValue = getGoogleConsentValue('ad_user_data') ?? getConsentValue(gdprPrivacyValueAdvertising)
const adPersonalizationConsentValue =
getGoogleConsentValue('ad_personalization') ?? getConsentValue(gdprPrivacyValueAdvertising)
const adStorageConsentValue = getGoogleConsentValue('ad_storage') ?? getConsentValue(gdprPrivacyValueAdvertising)

if (!isGdprAccepted) {
context.integrations = {
Expand All @@ -165,25 +176,51 @@ export const decorateContextWithNeededData = async ({event = '', context = {}})
}
}

return {
// Build Google Consent Mode object
const googleConsents = {
analytics_storage: useSegmentGA4Destination
? getConsentValue(gdprPrivacyValueAnalytics)
: getGoogleConsentValue('analytics_storage') ?? getConsentValue(gdprPrivacyValueAnalytics),
ad_storage: useSegmentGA4Destination
? getConsentValue(gdprPrivacyValueAdvertising)
: getGoogleConsentValue('ad_storage') ?? getConsentValue(gdprPrivacyValueAdvertising),
ad_user_data: useSegmentGA4Destination
? getConsentValue(gdprPrivacyValueAdvertising)
: getGoogleConsentValue('ad_user_data') ?? getConsentValue(gdprPrivacyValueAdvertising),
ad_personalization: useSegmentGA4Destination
? getConsentValue(gdprPrivacyValueAdvertising)
: getGoogleConsentValue('ad_personalization') ?? getConsentValue(gdprPrivacyValueAdvertising)
}

const baseContext = {
...context,
...(!isGdprAccepted && {ip: '0.0.0.0'}),
...getExternalIds({context, xandrId}),
analytics_storage: getConsentState(),
clientVersion: `segment-wrapper@${process.env.VERSION ?? '0.0.0'}`,
gdpr_privacy: gdprPrivacyValueAnalytics,
gdpr_privacy_advertising: gdprPrivacyValueAdvertising,
google_consents: {
analytics_storage: analyticsConsentValue,
ad_user_data: adUserDataConsentValue,
ad_personalization: adPersonalizationConsentValue,
ad_storage: adStorageConsentValue
},
integrations: {
...context.integrations,
...integrations
}
}

// Legacy behavior: add analytics_storage and google_consents to context
if (!useSegmentGA4Destination) {
baseContext.analytics_storage = getConsentState()
baseContext.google_consents = googleConsents
}

return {
context: baseContext,
properties: useSegmentGA4Destination
? {
...properties,
// New mode: add google_consents to properties
google_consents: googleConsents
}
: properties
}
}

/**
Expand All @@ -197,17 +234,21 @@ export const decorateContextWithNeededData = async ({event = '', context = {}})
const track = (event, properties, context = {}, callback) =>
new Promise(resolve => {
const initTrack = async () => {
const newContext = await decorateContextWithNeededData({context, event})

/**
* @deprecated Now we use `defaultContextProperties` middleware
* and put the info on the context object
*/
const newProperties = {
const baseProperties = {
...getDefaultProperties(),
...properties
}

const {context: newContext, properties: decoratedProperties} = await decorateContextWithNeededData({
context,
event,
properties: baseProperties
})

const newCallback = async (...args) => {
if (callback) callback(...args) // eslint-disable-line n/no-callback-literal
const [gdprPrivacyValue] = await Promise.all([getGdprPrivacyValue()])
Expand All @@ -219,17 +260,20 @@ const track = (event, properties, context = {}, callback) =>
}
}

const useSegmentGA4Destination = isGA4DestinationEnabled()
const needsConsentManagement = getConfig('googleAnalyticsConsentManagement')

if (needsConsentManagement && event === CMP_TRACK_EVENT) {
// Legacy behavior: send consents to GTM
if (!useSegmentGA4Destination && needsConsentManagement && event === CMP_TRACK_EVENT) {
sendGoogleConsents('update', newContext.google_consents)
}

window.analytics.track(
event,
newProperties,
decoratedProperties,
{
...newContext,
integrations: newContext.integrations,
context: {
integrations: {
...newContext.integrations
Expand All @@ -256,7 +300,10 @@ const identify = async (userIdParam, traits, options, callback) => {

const userId = getUserId(userIdParam)

setGoogleUserId(userId)
// Legacy behavior: set user ID in gtag
if (!isGA4DestinationEnabled()) {
setGoogleUserId(userId)
}

return window.analytics.identify(
userId,
Expand Down
1 change: 1 addition & 0 deletions packages/sui-segment-wrapper/src/tcf.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const trackTcf = ({eventId, gdprPrivacy}) =>
export const getGdprPrivacyValue = () => {
// try to get the actual gdprPrivacyValue and just return it
const gdprPrivacyValue = gdprState.get()

if (gdprPrivacyValue !== undefined) return Promise.resolve(gdprPrivacyValue)

// // if we don't have a gdprPrivacyValue, then subscribe to it until we have a value
Expand Down
22 changes: 22 additions & 0 deletions packages/sui-segment-wrapper/src/utils/ga4Detection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {getConfig} from '../config.js'

/**
* Check if Google Analytics 4 Web destination is enabled in Segment
* Simple detection based on googleAnalyticsMeasurementId presence:
* - If googleAnalyticsMeasurementId is configured → LEGACY mode (manual GA4)
* - If NOT configured → NEW mode (Segment destination handles GA4)
* @returns {boolean}
*/
export const isGA4DestinationEnabled = () => {
const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
// If no measurement ID configured, assume they're using Segment destination
return !googleAnalyticsMeasurementId
}

/**
* Reset the cached detection (useful for testing)
* @deprecated No longer needed as detection is now stateless
*/
export const resetGA4DetectionCache = () => {
// No-op: detection is now stateless, no cache to reset
}
2 changes: 0 additions & 2 deletions packages/sui-segment-wrapper/test/assertions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {expect} from 'chai'
import sinon from 'sinon'

import {setConfig} from '../src/config.js'
import suiAnalytics from '../src/index.js'
import {getCampaignDetails} from '../src/repositories/googleRepository.js'
import {stubActualQueryString} from './stubs.js'
Expand All @@ -12,7 +11,6 @@ export const assertCampaignDetails = async ({queryString, expectation}) => {
try {
const spy = sinon.stub()

setConfig('googleAnalyticsMeasurementId', 123)
await simulateUserAcceptConsents()
await suiAnalytics.track(
'fakeEvent',
Expand Down
Loading
Loading