@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.
- package/README.md +269 -2
- package/lib/config.js +20 -0
- package/lib/events.js +3 -0
- package/lib/index.js +82 -0
- package/lib/middlewares/destination/optimizelySiteAttribute.js +17 -0
- package/lib/middlewares/source/campaignContext.js +10 -0
- package/lib/middlewares/source/defaultContextProperties.js +11 -0
- package/lib/middlewares/source/optimizelyUserId.js +15 -0
- package/lib/middlewares/source/pageData.js +42 -0
- package/lib/middlewares/source/pageReferrer.js +102 -0
- package/lib/middlewares/source/userScreenInfo.js +12 -0
- package/lib/middlewares/source/userTraits.js +48 -0
- package/lib/repositories/googleRepository.js +272 -0
- package/lib/repositories/xandrRepository.js +42 -0
- package/lib/segmentWrapper.js +364 -0
- package/lib/tcf.js +230 -0
- package/lib/universalId.js +48 -0
- package/lib/utils/checkAnonymousId.js +9 -0
- package/lib/utils/cookies.js +50 -0
- package/lib/utils/ga4Detection.js +18 -0
- package/lib/utils/hashEmail.js +24 -0
- package/lib/utils/patchAnalytics.js +36 -0
- package/lib/utils/storage.js +17 -0
- package/package.json +39 -4
- package/src/config.js +25 -0
- package/src/events.js +3 -0
- package/src/index.js +96 -0
- package/src/middlewares/destination/optimizelySiteAttribute.js +22 -0
- package/src/middlewares/source/campaignContext.js +12 -0
- package/src/middlewares/source/defaultContextProperties.js +15 -0
- package/src/middlewares/source/optimizelyUserId.js +20 -0
- package/src/middlewares/source/pageData.js +45 -0
- package/src/middlewares/source/pageReferrer.js +125 -0
- package/src/middlewares/source/userScreenInfo.js +11 -0
- package/src/middlewares/source/userTraits.js +41 -0
- package/src/repositories/googleRepository.js +251 -0
- package/src/repositories/xandrRepository.js +48 -0
- package/src/segmentWrapper.js +338 -0
- package/src/tcf.js +263 -0
- package/src/universalId.js +51 -0
- package/src/utils/checkAnonymousId.js +10 -0
- package/src/utils/cookies.js +56 -0
- package/src/utils/ga4Detection.js +22 -0
- package/src/utils/hashEmail.js +33 -0
- package/src/utils/patchAnalytics.js +46 -0
- package/src/utils/storage.js +18 -0
- package/src-umd/index.js +8 -0
- 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
|
+
}
|