@ohos-ports/s-ui-segment-wrapper 0.0.0-stage → 5.4.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +269 -2
  2. package/lib/config.js +20 -0
  3. package/lib/events.js +3 -0
  4. package/lib/index.js +82 -0
  5. package/lib/middlewares/destination/optimizelySiteAttribute.js +17 -0
  6. package/lib/middlewares/source/campaignContext.js +10 -0
  7. package/lib/middlewares/source/defaultContextProperties.js +11 -0
  8. package/lib/middlewares/source/optimizelyUserId.js +15 -0
  9. package/lib/middlewares/source/pageData.js +42 -0
  10. package/lib/middlewares/source/pageReferrer.js +102 -0
  11. package/lib/middlewares/source/userScreenInfo.js +12 -0
  12. package/lib/middlewares/source/userTraits.js +48 -0
  13. package/lib/repositories/googleRepository.js +272 -0
  14. package/lib/repositories/xandrRepository.js +42 -0
  15. package/lib/segmentWrapper.js +364 -0
  16. package/lib/tcf.js +230 -0
  17. package/lib/universalId.js +48 -0
  18. package/lib/utils/checkAnonymousId.js +9 -0
  19. package/lib/utils/cookies.js +50 -0
  20. package/lib/utils/ga4Detection.js +18 -0
  21. package/lib/utils/hashEmail.js +24 -0
  22. package/lib/utils/patchAnalytics.js +36 -0
  23. package/lib/utils/storage.js +17 -0
  24. package/package.json +39 -4
  25. package/src/config.js +25 -0
  26. package/src/events.js +3 -0
  27. package/src/index.js +96 -0
  28. package/src/middlewares/destination/optimizelySiteAttribute.js +22 -0
  29. package/src/middlewares/source/campaignContext.js +12 -0
  30. package/src/middlewares/source/defaultContextProperties.js +15 -0
  31. package/src/middlewares/source/optimizelyUserId.js +20 -0
  32. package/src/middlewares/source/pageData.js +45 -0
  33. package/src/middlewares/source/pageReferrer.js +125 -0
  34. package/src/middlewares/source/userScreenInfo.js +11 -0
  35. package/src/middlewares/source/userTraits.js +41 -0
  36. package/src/repositories/googleRepository.js +251 -0
  37. package/src/repositories/xandrRepository.js +48 -0
  38. package/src/segmentWrapper.js +338 -0
  39. package/src/tcf.js +263 -0
  40. package/src/universalId.js +51 -0
  41. package/src/utils/checkAnonymousId.js +10 -0
  42. package/src/utils/cookies.js +56 -0
  43. package/src/utils/ga4Detection.js +22 -0
  44. package/src/utils/hashEmail.js +33 -0
  45. package/src/utils/patchAnalytics.js +46 -0
  46. package/src/utils/storage.js +18 -0
  47. package/src-umd/index.js +8 -0
  48. package/umd/index.js +1 -0
@@ -0,0 +1,338 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ CONSENT_STATES,
5
+ getConsentState,
6
+ getGoogleClientId,
7
+ getGoogleConsentValue,
8
+ getGoogleSessionId,
9
+ sendGoogleConsents,
10
+ setGoogleUserId
11
+ } from './repositories/googleRepository.js'
12
+ import {getXandrId} from './repositories/xandrRepository.js'
13
+ import {getConfig} from './config.js'
14
+ import {USER_GDPR, CMP_TRACK_EVENT, checkAnalyticsGdprIsAccepted, getGdprPrivacyValue} from './tcf.js'
15
+ import {isGA4DestinationEnabled} from './utils/ga4Detection.js'
16
+
17
+ /* Default properties to be sent on all trackings */
18
+ const DEFAULT_PROPERTIES = {platform: 'web'}
19
+
20
+ /* Disabled integrations when no GDPR Privacy Value is true */
21
+ export const INTEGRATIONS_WHEN_NO_CONSENTS = {
22
+ All: false
23
+ }
24
+
25
+ export const INTEGRATIONS_WHEN_NO_CONSENTS_CMP_SUBMITTED = {
26
+ All: true
27
+ }
28
+
29
+ const getUserId = userId => {
30
+ const prefix = getConfig('userIdPrefix')
31
+
32
+ if (!prefix) {
33
+ return userId
34
+ }
35
+
36
+ if (typeof userId === 'number' || (typeof userId === 'string' && !userId.startsWith(prefix))) {
37
+ return `${prefix}${userId}`
38
+ }
39
+
40
+ return userId
41
+ }
42
+
43
+ /**
44
+ * Get default properties using the constant and the window.__mpi object if available
45
+ * @returns {{[key:string]: any}} Default properties to attach to track
46
+ */
47
+ export const getDefaultProperties = () => ({
48
+ ...DEFAULT_PROPERTIES,
49
+ ...getConfig('defaultProperties')
50
+ })
51
+
52
+ /**
53
+ * Get all needed integrations depending on the gdprPrivacy value.
54
+ * @param {object} param - Object with the gdprPrivacyValue and if it's a CMP Submitted event
55
+ */
56
+ const getTrackIntegrations = async ({gdprPrivacyValue, event}) => {
57
+ const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue)
58
+ const restOfIntegrations = getRestOfIntegrations({isGdprAccepted, event})
59
+
60
+ // Check if we should use manual GA4 (legacy) or Segment destination (new)
61
+ const useSegmentGA4Destination = isGA4DestinationEnabled()
62
+
63
+ if (useSegmentGA4Destination) {
64
+ // New behavior: let Segment handle GA4
65
+ return {
66
+ ...restOfIntegrations,
67
+ 'Google Analytics 4 Web': true
68
+ }
69
+ }
70
+
71
+ // Legacy behavior: manual GA4 with clientId/sessionId
72
+ let sessionId
73
+ let clientId
74
+
75
+ try {
76
+ sessionId = await getGoogleSessionId()
77
+ clientId = await getGoogleClientId()
78
+ } catch (error) {
79
+ console.error(
80
+ '[segment-wrapper] Failed to retrieve GA4 session/client IDs. Events will be sent without session attribution.',
81
+ error
82
+ )
83
+ }
84
+
85
+ return {
86
+ ...restOfIntegrations,
87
+ 'Google Analytics 4':
88
+ clientId && sessionId
89
+ ? {
90
+ clientId,
91
+ sessionId
92
+ }
93
+ : true
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get Rest of integrations depending on the gdprPrivacy value and if it's a CMP Submitted event
99
+ * @param {object} param - Object with the isGdprAccepted and if it's a CMP Submitted event
100
+ * @returns {object} integrations
101
+ */
102
+ export const getRestOfIntegrations = ({isGdprAccepted, event}) => {
103
+ const isCMPSubmittedEvent = event === 'CMP Submitted'
104
+
105
+ if (isCMPSubmittedEvent) {
106
+ return INTEGRATIONS_WHEN_NO_CONSENTS_CMP_SUBMITTED
107
+ }
108
+ return isGdprAccepted ? {} : INTEGRATIONS_WHEN_NO_CONSENTS
109
+ }
110
+
111
+ /**
112
+ * It returns externalIds to add to context
113
+ *
114
+ * @param {Object} param
115
+ * @param {Object} param.context previous context
116
+ * @param {String} param.xandrId xandrId to be included
117
+ * @returns
118
+ */
119
+ const getExternalIds = ({context, xandrId}) => {
120
+ const shouldSendXandrId = getConfig('sendXandrId') !== false
121
+ const isValidXandrId = xandrId && parseInt(xandrId) !== 0
122
+ if (!shouldSendXandrId || !isValidXandrId) {
123
+ return {}
124
+ }
125
+ const SEGMENT_COLLECTION = 'users'
126
+ const SEGMENT_ENCODING = 'none'
127
+ const SEGMENT_TYPE = 'xandr_id'
128
+ const externalIds = [
129
+ ...(context?.externalIds || []),
130
+ {
131
+ collection: SEGMENT_COLLECTION,
132
+ encoding: SEGMENT_ENCODING,
133
+ id: xandrId,
134
+ type: SEGMENT_TYPE
135
+ }
136
+ ]
137
+
138
+ const uniqueExternalIds = externalIds.filter(
139
+ ({id: idFilter, type: typeFilter}, index) =>
140
+ index === externalIds.findIndex(({id: idFind, type: typeFind}) => idFilter === idFind && typeFilter === typeFind)
141
+ )
142
+ return {externalIds: uniqueExternalIds}
143
+ }
144
+
145
+ /**
146
+ * Get consent value for Google Consent Mode
147
+ * @param {string} gdprValue
148
+ * @returns {string} consent value
149
+ */
150
+ const getConsentValue = gdprValue => (gdprValue === USER_GDPR.ACCEPTED ? CONSENT_STATES.granted : CONSENT_STATES.denied)
151
+
152
+ /**
153
+ * Get data like traits and integrations to be added to the context object
154
+ * @param {object} context Context object with all the actual info
155
+ * @returns {Promise<object>} New context with all the previous info and the new one
156
+ */
157
+ export const decorateContextWithNeededData = async ({event = '', context = {}, properties = {}}) => {
158
+ const gdprPrivacyValue = await getGdprPrivacyValue()
159
+ const {analytics: gdprPrivacyValueAnalytics, advertising: gdprPrivacyValueAdvertising} = gdprPrivacyValue || {}
160
+ const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue)
161
+
162
+ // Check if we should use Segment destination or legacy mode
163
+ const useSegmentGA4Destination = isGA4DestinationEnabled()
164
+
165
+ const [integrations, xandrId] = await Promise.all([
166
+ getTrackIntegrations({gdprPrivacyValue, event}),
167
+ getXandrId({gdprPrivacyValueAdvertising})
168
+ ])
169
+
170
+ // Build integrations without mutating context
171
+ const contextIntegrations = !isGdprAccepted
172
+ ? {
173
+ ...(context.integrations ?? {}),
174
+ Personas: false,
175
+ Webhooks: true,
176
+ Webhook: true
177
+ }
178
+ : context.integrations
179
+
180
+ // Build Google Consent Mode object
181
+ const googleConsents = {
182
+ analytics_storage: useSegmentGA4Destination
183
+ ? getConsentValue(gdprPrivacyValueAnalytics)
184
+ : getGoogleConsentValue('analytics_storage') ?? getConsentValue(gdprPrivacyValueAnalytics),
185
+ ad_storage: useSegmentGA4Destination
186
+ ? getConsentValue(gdprPrivacyValueAdvertising)
187
+ : getGoogleConsentValue('ad_storage') ?? getConsentValue(gdprPrivacyValueAdvertising),
188
+ ad_user_data: useSegmentGA4Destination
189
+ ? getConsentValue(gdprPrivacyValueAdvertising)
190
+ : getGoogleConsentValue('ad_user_data') ?? getConsentValue(gdprPrivacyValueAdvertising),
191
+ ad_personalization: useSegmentGA4Destination
192
+ ? getConsentValue(gdprPrivacyValueAdvertising)
193
+ : getGoogleConsentValue('ad_personalization') ?? getConsentValue(gdprPrivacyValueAdvertising)
194
+ }
195
+
196
+ const baseContext = {
197
+ ...context,
198
+ ...(!isGdprAccepted && {ip: '0.0.0.0'}),
199
+ ...getExternalIds({context, xandrId}),
200
+ clientVersion: `segment-wrapper@${process.env.VERSION ?? '0.0.0'}`,
201
+ gdpr_privacy: gdprPrivacyValueAnalytics,
202
+ gdpr_privacy_advertising: gdprPrivacyValueAdvertising,
203
+ integrations: {
204
+ ...contextIntegrations,
205
+ ...integrations
206
+ }
207
+ }
208
+
209
+ // Legacy behavior: add analytics_storage and google_consents to context
210
+ if (!useSegmentGA4Destination) {
211
+ baseContext.analytics_storage = getConsentState()
212
+ baseContext.google_consents = googleConsents
213
+ }
214
+
215
+ return {
216
+ context: baseContext,
217
+ properties: useSegmentGA4Destination
218
+ ? {
219
+ ...properties,
220
+ // New mode: add google_consents to properties
221
+ google_consents: googleConsents
222
+ }
223
+ : properties
224
+ }
225
+ }
226
+
227
+ /**
228
+ * The track method lets you record any actions your users perform.
229
+ * @param {string} event The name of the event you’re tracking
230
+ * @param {object} [properties] A dictionary of properties for the event.
231
+ * @param {object} [context] A dictionary of options.
232
+ * @param {function} [callback] A function executed after a short timeout, giving the browser time to make outbound requests first.
233
+ * @returns {Promise}
234
+ */
235
+ const track = (event, properties, context = {}, callback) =>
236
+ new Promise(resolve => {
237
+ const initTrack = async () => {
238
+ /**
239
+ * @deprecated Now we use `defaultContextProperties` middleware
240
+ * and put the info on the context object
241
+ */
242
+ const baseProperties = {
243
+ ...getDefaultProperties(),
244
+ ...properties
245
+ }
246
+
247
+ const {context: newContext, properties: decoratedProperties} = await decorateContextWithNeededData({
248
+ context,
249
+ event,
250
+ properties: baseProperties
251
+ })
252
+
253
+ const newCallback = async (...args) => {
254
+ if (callback) callback(...args) // eslint-disable-line n/no-callback-literal
255
+ const [gdprPrivacyValue] = await Promise.all([getGdprPrivacyValue()])
256
+
257
+ if (checkAnalyticsGdprIsAccepted(gdprPrivacyValue)) {
258
+ resolve(...args)
259
+ } else {
260
+ resolve()
261
+ }
262
+ }
263
+
264
+ const useSegmentGA4Destination = isGA4DestinationEnabled()
265
+ const needsConsentManagement = getConfig('googleAnalyticsConsentManagement')
266
+
267
+ // Legacy behavior: send consents to GTM
268
+ if (!useSegmentGA4Destination && needsConsentManagement && event === CMP_TRACK_EVENT) {
269
+ sendGoogleConsents('update', newContext.google_consents)
270
+ }
271
+
272
+ window.analytics.track(
273
+ event,
274
+ decoratedProperties,
275
+ {
276
+ ...newContext,
277
+ context: {
278
+ integrations: {
279
+ ...newContext.integrations
280
+ }
281
+ }
282
+ },
283
+ newCallback
284
+ )
285
+ }
286
+
287
+ initTrack()
288
+ })
289
+
290
+ /**
291
+ * Associate your users and their actions to a recognizable userId and traits.
292
+ * @param {string} userIdParam Id to identify the user.
293
+ * @param {object} traits A dictionary of traits you know about the user, like their email or name.
294
+ * @param {object} [options] A dictionary of options.
295
+ * @param {function} [callback] A function executed after a short timeout, giving the browser time to make outbound requests first.
296
+ * @returns {Promise}
297
+ */
298
+ const identify = async (userIdParam, traits, options, callback) => {
299
+ const gdprPrivacyValue = await getGdprPrivacyValue()
300
+
301
+ const userId = getUserId(userIdParam)
302
+
303
+ // Legacy behavior: set user ID in gtag
304
+ if (!isGA4DestinationEnabled()) {
305
+ setGoogleUserId(userId)
306
+ }
307
+
308
+ return window.analytics.identify(
309
+ userId,
310
+ checkAnalyticsGdprIsAccepted(gdprPrivacyValue) ? traits : {},
311
+ options,
312
+ callback
313
+ )
314
+ }
315
+
316
+ /**
317
+ * Record whenever a user sees a page of your website, along with any optional properties about the page.
318
+ * @param {string} event The name of the event you’re tracking
319
+ * @param {object=} properties A dictionary of properties for the event.
320
+ * @param {object} [context] A dictionary of options.
321
+ * @param {function} [callback] A function executed after a short timeout, giving the browser time to make outbound requests first.
322
+ * @returns {Promise}
323
+ */
324
+ const page = (event, properties, context = {}, callback) => {
325
+ // we put a flag on context to know this track is a page
326
+ context.isPageTrack = true
327
+ // just call track again but the with the proper context
328
+ return track(event, properties, context, callback)
329
+ }
330
+
331
+ /**
332
+ * Resets the id, including anonymousId, and clear traits for the currently identified user and group.
333
+ * NOTE: Only clears the cookies and localStorage set by analytics.
334
+ * @returns {Promise}
335
+ */
336
+ const reset = () => Promise.resolve(window.analytics.reset())
337
+
338
+ export default {page, identify, track, reset}
package/src/tcf.js ADDED
@@ -0,0 +1,263 @@
1
+ import {getConfig, isClient, setConfig} from './config.js'
2
+ import analytics from './segmentWrapper.js'
3
+
4
+ /**
5
+ * Cookie to extract the already saved consents for the user
6
+ * @type {string}
7
+ */
8
+ const TCF_COOKIE_KEY = 'borosTcf'
9
+ /**
10
+ * TCF Api Version to use
11
+ * @type {number}
12
+ */
13
+ const TCF_API_VERSION = 2
14
+
15
+ /**
16
+ * Default properties to send with every TCF tracking event
17
+ */
18
+ const TCF_TRACK_PROPERTIES = {
19
+ channel: 'GDPR'
20
+ }
21
+
22
+ /**
23
+ * List of purpose ids needed to be able to track with all info
24
+ * @type {Array<number>}
25
+ */
26
+ const NEEDED_PURPOSES = {
27
+ analytics: [1, 8, 10],
28
+ advertising: [3]
29
+ }
30
+
31
+ /**
32
+ * TCF events
33
+ */
34
+ const TCF_EVENTS = {
35
+ // Event that determines that the tcData has been loaded
36
+ LOADED: 'tcloaded',
37
+ // Event that determines that the user has performed an action
38
+ USER_ACTION_COMPLETE: 'useractioncomplete'
39
+ }
40
+
41
+ export const CMP_TRACK_EVENT = 'CMP Submitted'
42
+
43
+ /**
44
+ * State of user according to GDPR regarding tracking
45
+ */
46
+ export const USER_GDPR = {
47
+ ACCEPTED: 'accepted',
48
+ DECLINED: 'declined',
49
+ UNKNOWN: 'unknown'
50
+ }
51
+
52
+ /**
53
+ * Define the user GDPR consents state. This value will be updated with new values
54
+ * when the consents of the users changes.
55
+ */
56
+ const gdprState = {
57
+ listeners: [],
58
+ value: undefined,
59
+ addListener: callback => gdprState.listeners.push(callback),
60
+ get: () => gdprState.value,
61
+ set: value => {
62
+ const {analytics, advertising} = value || {}
63
+ gdprState.value = {
64
+ analytics,
65
+ advertising
66
+ }
67
+ gdprState.listeners.forEach(fn => fn(value))
68
+ gdprState.listeners = []
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Read cookie by using a key
74
+ * @returns {string}
75
+ */
76
+ function readCookie(key) {
77
+ const re = new RegExp(key + '=([^;]+)')
78
+ const value = re.exec(document.cookie)
79
+ return value !== null ? unescape(value[1]) : null
80
+ }
81
+
82
+ /**
83
+ * Check if we're on client and tcfApi is available on window object
84
+ * @returns {Boolean}
85
+ */
86
+ const checkTcfIsAvailable = () => {
87
+ // if we're on the client, check we haven't already initialized it
88
+ if (getConfig('initialized')) return false
89
+
90
+ // if we're on client, check if we have the tcfapi available
91
+ const isTcfApiAvailable = !!window.__tcfapi
92
+ !isTcfApiAvailable &&
93
+ console.warn("[tcfTracking] window.__tcfapi is not available on client and TCF won't be tracked.")
94
+ return isTcfApiAvailable
95
+ }
96
+
97
+ /**
98
+ * Check from a list of consents if user has accepted being tracked
99
+ * @param {{[purposeId: string]: boolean}} userConsents
100
+ * @returns {Boolean}
101
+ */
102
+ const checkHasUserConsentedAnalytics = userConsents =>
103
+ NEEDED_PURPOSES.analytics.every(purposeId => userConsents[`${purposeId}`])
104
+
105
+ const checkHasUserConsentedAdvertising = userConsents =>
106
+ NEEDED_PURPOSES.advertising.every(purposeId => userConsents[`${purposeId}`])
107
+
108
+ /**
109
+ * Track a specific TCF event
110
+ * @param {object} params
111
+ * @param {string} params.eventId Event ID to be sent with the TCF Tracking
112
+ * @param {string} params.gdprPrivacy Send a string telling if the gdpr has been accepted or reject
113
+ * @return {Promise}
114
+ */
115
+ const trackTcf = ({eventId, gdprPrivacy}) =>
116
+ analytics.track(
117
+ CMP_TRACK_EVENT,
118
+ {
119
+ ...TCF_TRACK_PROPERTIES,
120
+ ...getConfig('tcfTrackDefaultProperties')
121
+ },
122
+ {
123
+ gdpr_privacy: gdprPrivacy.analytics,
124
+ gdpr_privacy_advertising: gdprPrivacy.advertising
125
+ }
126
+ )
127
+
128
+ /**
129
+ * Get if we have user consents
130
+ * @return {Promise<Object>}
131
+ */
132
+ export const getGdprPrivacyValue = () => {
133
+ // try to get the actual gdprPrivacyValue and just return it
134
+ const gdprPrivacyValue = gdprState.get()
135
+
136
+ if (gdprPrivacyValue !== undefined) return Promise.resolve(gdprPrivacyValue)
137
+
138
+ // // if we don't have a gdprPrivacyValue, then subscribe to it until we have a value
139
+ return new Promise(resolve => {
140
+ gdprState.addListener(gdprPrivacyValue => resolve(gdprPrivacyValue))
141
+ })
142
+ }
143
+
144
+ /**
145
+ * Check if gdprPrivacyValue is accepted
146
+ * @return {boolean}
147
+ */
148
+ export const checkAnalyticsGdprIsAccepted = gdprPrivacyValue => {
149
+ return gdprPrivacyValue.analytics === USER_GDPR.ACCEPTED
150
+ }
151
+
152
+ /**
153
+ * Set gdprState according to list of purpose consents
154
+ * @returns {string}
155
+ */
156
+ const setGdprStateBy = purposeConsents => {
157
+ const hasAnalyticsConsents = checkHasUserConsentedAnalytics(purposeConsents)
158
+ const hasAdvertisingConsents = checkHasUserConsentedAdvertising(purposeConsents)
159
+ // get the state key according to the gdprPrivacyValue
160
+ const gdprAnalyticsStateKey = hasAnalyticsConsents ? USER_GDPR.ACCEPTED : USER_GDPR.DECLINED
161
+
162
+ const gdprAdvertisingStateKey = hasAdvertisingConsents ? USER_GDPR.ACCEPTED : USER_GDPR.DECLINED
163
+ // update gdprState with the new value for user gdpr
164
+ gdprState.set({
165
+ analytics: gdprAnalyticsStateKey,
166
+ advertising: gdprAdvertisingStateKey
167
+ })
168
+ // return the new gdprState
169
+ return {
170
+ analytics: gdprAnalyticsStateKey,
171
+ advertising: gdprAdvertisingStateKey
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Read and decode the tcf cookie
177
+ */
178
+ const getConsentsFromCookie = () => {
179
+ const cookieValue = readCookie(TCF_COOKIE_KEY)
180
+ if (!cookieValue) return
181
+
182
+ try {
183
+ const {purpose} = JSON.parse(window.atob(cookieValue))
184
+ const {consents} = purpose
185
+ return consents
186
+ } catch (e) {
187
+ console.error(e)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Set the correct initial gdprState based on the consents cookie string
193
+ */
194
+ const initializeGdprState = () => {
195
+ const consents = getConsentsFromCookie()
196
+ if (consents) setGdprStateBy(consents)
197
+ }
198
+
199
+ /**
200
+ * Sets global isFirstVisit flag based on the consents cookie string
201
+ */
202
+ const initializeIsFirstVisit = () => {
203
+ const consents = getConsentsFromCookie()
204
+ setConfig('isFirstVisit', !consents)
205
+ }
206
+
207
+ /**
208
+ * Init TCF Tracking User Consents with Segment
209
+ */
210
+ export default function initTcfTracking() {
211
+ // first check if we're on server, as this doesn't work on server-side
212
+ if (!isClient) return
213
+ // read the cookie and put the correct usergdprValue and isFirstVisit flag before listening events
214
+ initializeGdprState()
215
+ initializeIsFirstVisit()
216
+ // do some checks before initializing tcf tracking as we do that only if available
217
+ if (!checkTcfIsAvailable()) {
218
+ // if we don't have a gdpr state and tcf is not available
219
+ // we should assume we don't known if we have consents
220
+ const analyticsGdprState = gdprState.get()?.analytics
221
+ if (analyticsGdprState === undefined)
222
+ gdprState.set({
223
+ analytics: USER_GDPR.UNKNOWN,
224
+ advertising: USER_GDPR.UNKNOWN
225
+ })
226
+ // and we stop executing as we can't track tcf
227
+ return
228
+ }
229
+ // add a flag to the segmentWrapper config to know it's already initialized
230
+ setConfig('initialized', true)
231
+
232
+ // listen events from tcf api
233
+ window.__tcfapi('addEventListener', TCF_API_VERSION, ({eventStatus, purpose}, success) => {
234
+ if (!success) return Promise.resolve()
235
+
236
+ // if we've already user consents or the user is accepting or declining now
237
+ // we change the state of the GDPR to use in our tracking
238
+ if (eventStatus === TCF_EVENTS.USER_ACTION_COMPLETE || eventStatus === TCF_EVENTS.LOADED) {
239
+ const {consents} = purpose
240
+ const gdprStateKey = setGdprStateBy(consents)
241
+ // if it's a user action, then we will track it
242
+ if (eventStatus === TCF_EVENTS.USER_ACTION_COMPLETE) {
243
+ // extract the eventId and gdprPrivacy string to send with the track
244
+ const {analytics: analyticsStateKey, advertising: advertisingStateKey} = gdprStateKey
245
+
246
+ const gdprPrivacy = {
247
+ analytics: analyticsStateKey,
248
+ advertising: advertisingStateKey
249
+ }
250
+
251
+ // temporary during Didomi migration to avoid fake programmatically generated events where the users doesn't really interact
252
+ const MIGRATION_DIDOMI_SEGMENT_WRAPPER_FLAG = 'didomi-migration'
253
+ const isDidomiMigration = isClient && window.sessionStorage.getItem(MIGRATION_DIDOMI_SEGMENT_WRAPPER_FLAG)
254
+ return (
255
+ !isDidomiMigration &&
256
+ trackTcf({
257
+ gdprPrivacy
258
+ })
259
+ )
260
+ }
261
+ }
262
+ })
263
+ }
@@ -0,0 +1,51 @@
1
+ import {dispatchEvent} from '@s-ui/js/lib/events'
2
+
3
+ import {createUniversalId} from './utils/hashEmail.js'
4
+ import {getConfig, isClient, setConfig} from './config.js'
5
+
6
+ const USER_DATA_READY_EVENT = 'USER_DATA_READY'
7
+
8
+ export const getUniversalIdFromConfig = () => getConfig('universalId')
9
+
10
+ export const getUniversalId = () => {
11
+ // 1. Try to get universalId from config
12
+ let universalId = getUniversalIdFromConfig()
13
+
14
+ if (universalId) {
15
+ setUniversalIdInitialized()
16
+
17
+ return universalId
18
+ }
19
+
20
+ // 2. If not available, then we use the email and hash it
21
+ const userEmail = getConfig('userEmail')
22
+
23
+ if (userEmail) {
24
+ universalId = createUniversalId(userEmail)
25
+ setUniversalId(universalId)
26
+ setUniversalIdInitialized()
27
+
28
+ return universalId
29
+ }
30
+
31
+ // 3. We don't have user email, so we don't have universalId
32
+ // but we've tried, so we set it as initialized
33
+ setUniversalIdInitialized()
34
+ }
35
+
36
+ export const getUserDataAndNotify = () => {
37
+ if (!isClient) return
38
+
39
+ const universalId = getUniversalId()
40
+ const userEmail = getConfig('userEmail')
41
+
42
+ dispatchEvent({eventName: USER_DATA_READY_EVENT, detail: {universalId, userEmail}})
43
+
44
+ return {universalId, userEmail}
45
+ }
46
+
47
+ export const setUniversalIdInitialized = () => {
48
+ setConfig('universalIdInitialized', true)
49
+ }
50
+
51
+ export const setUniversalId = universalId => setConfig('universalId', universalId)
@@ -0,0 +1,10 @@
1
+ export const checkAnonymousId = () => {
2
+ const SEGMENT_ID_USER_WITHOUT_CONSENTS =
3
+ window.__SEGMENT_WRAPPER?.SEGMENT_ID_USER_WITHOUT_CONSENTS ?? 'anonymous_user'
4
+
5
+ const anonymousId = window.analytics.user?.()?.anonymousId()
6
+
7
+ if (anonymousId === SEGMENT_ID_USER_WITHOUT_CONSENTS) {
8
+ window.analytics.setAnonymousId(null)
9
+ }
10
+ }