@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,20 @@
1
+ export const optimizelyUserId = ({payload, next}) => {
2
+ const {
3
+ obj: {integrations}
4
+ } = payload
5
+
6
+ // when integrations.All equals false, destinations are deactivated
7
+ if (integrations.All === false) {
8
+ return next(payload)
9
+ }
10
+
11
+ payload.obj.integrations = {
12
+ ...integrations,
13
+ Optimizely: {
14
+ userId: window.analytics.user().anonymousId(),
15
+ ...integrations.Optimizely
16
+ }
17
+ }
18
+
19
+ next(payload)
20
+ }
@@ -0,0 +1,45 @@
1
+ export const createPageDataMiddleware = () => {
2
+ let last = null
3
+
4
+ return ({payload, next}) => {
5
+ try {
6
+ const {obj} = payload
7
+ const {event, type} = obj
8
+
9
+ if (type !== 'track') {
10
+ next(payload)
11
+ return
12
+ }
13
+
14
+ if (typeof event === 'string' && event.endsWith('Viewed')) {
15
+ const {page_name: name, page_type: type} = obj.properties || {}
16
+ last = {name, type}
17
+
18
+ next(payload)
19
+ return
20
+ }
21
+
22
+ if (!last) {
23
+ next(payload)
24
+ return
25
+ }
26
+
27
+ next({
28
+ ...payload,
29
+ obj: {
30
+ ...obj,
31
+ properties: {
32
+ ...obj.properties,
33
+ page_name_origin: last.name,
34
+ page_type: obj.properties?.page_type || last.type
35
+ }
36
+ }
37
+ })
38
+ } catch (error) {
39
+ console.error(error) // eslint-disable-line
40
+ next(payload)
41
+ }
42
+ }
43
+ }
44
+
45
+ export const pageData = createPageDataMiddleware()
@@ -0,0 +1,125 @@
1
+ import {isGA4DestinationEnabled} from '../../utils/ga4Detection.js'
2
+
3
+ /**
4
+ * Capture document.referrer at module load time, before Safari ITP can clear it.
5
+ * This ensures the first page event has the real referrer from the external source.
6
+ */
7
+ export const INITIAL_DOCUMENT_REFERRER = typeof document !== 'undefined' ? document.referrer : ''
8
+
9
+ /**
10
+ * Capture window.location.search at module load time, before Safari ITP can
11
+ * strip tracking parameters via redirects.
12
+ */
13
+ export const INITIAL_SEARCH_STRING = typeof window !== 'undefined' ? window.location.search : ''
14
+
15
+ /**
16
+ * Capture initial URL at module load time
17
+ */
18
+ const INITIAL_URL = typeof window !== 'undefined' ? window.location.href : ''
19
+
20
+ export const referrerState = {
21
+ spaReferrer: '',
22
+ referrer: INITIAL_DOCUMENT_REFERRER,
23
+ isFirstPageViewSent: false
24
+ }
25
+
26
+ /**
27
+ * Useful wrapper around document and window objects
28
+ */
29
+ export const utils = {
30
+ /**
31
+ * @returns {string} The referrer of the document
32
+ */
33
+ getDocumentReferrer: () => document.referrer,
34
+ /**
35
+ * @returns {string} The actual location with protocol, domain and pathname
36
+ */
37
+ getActualLocation: () => {
38
+ const {origin, pathname} = window.location
39
+ return `${origin}${pathname}`
40
+ },
41
+ /**
42
+ * @returns {string} The actual query string captured at module load (protected from Safari ITP)
43
+ */
44
+ getActualQueryString: () => {
45
+ // In tests using window.history.pushState, return current search if different from initial
46
+ if (typeof window !== 'undefined' && window.location.search && window.location.search !== INITIAL_SEARCH_STRING) {
47
+ return window.location.search
48
+ }
49
+ return INITIAL_SEARCH_STRING
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get the correct page referrer for SPA navigations
55
+ * @returns {string} referrer
56
+ */
57
+ export const getPageReferrer = ({isPageTrack = false} = {}) => {
58
+ const {referrer, spaReferrer} = referrerState
59
+
60
+ if (isPageTrack) {
61
+ // For page events, use referrer (initially from INITIAL_DOCUMENT_REFERRER, then from previous page location)
62
+ return referrer
63
+ } else {
64
+ // For track events, use spaReferrer if available, otherwise fall back to referrer
65
+ // This handles the case where a track happens before the first page event
66
+ return spaReferrer || referrer
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Update page referrer for SPA navigations
72
+ */
73
+ export const updatePageReferrer = () => {
74
+ referrerState.spaReferrer = getPageReferrer({isPageTrack: true})
75
+ // mutate actualReferrer with what will be the new referrer
76
+ referrerState.referrer = utils.getActualLocation()
77
+ }
78
+
79
+ /**
80
+ * @param {object} params
81
+ * @param {{obj: {context: object }}} params.payload
82
+ * @param {function} params.next
83
+ * @returns {void}
84
+ */
85
+ export const pageReferrer = ({payload, next}) => {
86
+ const {
87
+ obj: {context}
88
+ } = payload
89
+
90
+ const {isPageTrack} = context
91
+
92
+ const referrer = getPageReferrer({isPageTrack})
93
+
94
+ let props = {}
95
+
96
+ if (isPageTrack && !referrerState.isFirstPageViewSent) {
97
+ referrerState.isFirstPageViewSent = true
98
+ props = {
99
+ url: INITIAL_URL,
100
+ search: INITIAL_SEARCH_STRING
101
+ }
102
+ }
103
+
104
+ payload.obj.context = {
105
+ ...context,
106
+ page: {
107
+ ...context.page,
108
+ ...props,
109
+ referrer
110
+ }
111
+ }
112
+
113
+ // In NEW mode (client-side destination), also add referrer to properties
114
+ // GA4 Web destination uses properties.page_referrer for attribution
115
+ if (referrer && isGA4DestinationEnabled()) {
116
+ payload.obj.properties = {
117
+ ...payload.obj.properties,
118
+ page_referrer: referrer
119
+ }
120
+ }
121
+
122
+ // update page referrer only if it's a page event
123
+ isPageTrack && updatePageReferrer()
124
+ next(payload)
125
+ }
@@ -0,0 +1,11 @@
1
+ export const userScreenInfo = ({payload, next}) => {
2
+ payload.obj.context = {
3
+ ...payload.obj.context,
4
+ screen: {
5
+ height: window.innerHeight,
6
+ width: window.innerWidth,
7
+ density: window.devicePixelRatio
8
+ }
9
+ }
10
+ next(payload)
11
+ }
@@ -0,0 +1,41 @@
1
+ import {getConfig} from '../../config.js'
2
+ import {checkAnalyticsGdprIsAccepted, getGdprPrivacyValue} from '../../tcf.js'
3
+
4
+ /**
5
+ * Get user traits from global analytics object and put in the object
6
+ * @param {string} gdprPrivacyValue Determine if we have user consents
7
+ * @returns {object} User traits with to add
8
+ */
9
+ const getUserTraits = gdprPrivacyValue => {
10
+ const isUserTraitsEnabled = getConfig('isUserTraitsEnabled')
11
+ const user = window.analytics.user()
12
+ const isGdprAccepted = checkAnalyticsGdprIsAccepted(gdprPrivacyValue)
13
+ const userId = user.id()
14
+
15
+ return {
16
+ anonymousId: user.anonymousId(),
17
+ ...(userId && {userId}),
18
+ ...(isGdprAccepted && isUserTraitsEnabled && user.traits())
19
+ }
20
+ }
21
+
22
+ export const userTraits = async ({payload, next}) => {
23
+ const gdprPrivacyValue = await getGdprPrivacyValue()
24
+
25
+ let userTraits
26
+ try {
27
+ userTraits = getUserTraits(gdprPrivacyValue)
28
+ } catch (error) {
29
+ console.error(error) // eslint-disable-line
30
+ }
31
+
32
+ payload.obj.context = {
33
+ ...payload.obj.context,
34
+ traits: {
35
+ ...payload.obj.context.traits,
36
+ ...userTraits
37
+ }
38
+ }
39
+
40
+ next(payload)
41
+ }
@@ -0,0 +1,251 @@
1
+ import {dispatchEvent} from '@s-ui/js/lib/events'
2
+
3
+ import {getConfig} from '../config.js'
4
+ import {EVENTS} from '../events.js'
5
+ import {utils} from '../middlewares/source/pageReferrer.js'
6
+ import {isGA4DestinationEnabled} from '../utils/ga4Detection.js'
7
+
8
+ const FIELDS = {
9
+ clientId: 'client_id',
10
+ sessionId: 'session_id'
11
+ }
12
+
13
+ export const DEFAULT_DATA_LAYER_NAME = 'dataLayer'
14
+
15
+ export const CONSENT_STATES = {
16
+ granted: 'granted',
17
+ denied: 'denied'
18
+ }
19
+
20
+ const CONSENT_STATE_GRANTED_VALUE = 1
21
+ const CONSENT_STATE_DENIED_VALUE = 2
22
+
23
+ const STC = {
24
+ QUERY: 'stc',
25
+ SPLIT_SYMBOL: '-',
26
+ CAMPAIGN_SPLIT_SYMBOL: ':'
27
+ }
28
+
29
+ const UTM_CAMPAIGN = 'utm_campaign'
30
+ const UTM_MEDIUM = 'utm_medium'
31
+ const UTM_SOURCE = 'utm_source'
32
+ const UTM_CONTENT = 'utm_content'
33
+ const UTM_CAMPAIGN_ID = 'utm_id'
34
+ const UTM_TERM = 'utm_term'
35
+
36
+ const STC_MEDIUM_TRANSFORMATIONS = {
37
+ aff: 'affiliate',
38
+ dis: 'display',
39
+ em: 'email',
40
+ met: 'paid-metasearch',
41
+ sem: 'paid-search',
42
+ rt: 'retargeting',
43
+ sm: 'social-media',
44
+ sp: 'paid-social',
45
+ pn: 'push-notification',
46
+ cs: 'cross-sites'
47
+ }
48
+ const STC_INVALID_CONTENT = 'na'
49
+ const DEFAULT_GA_INIT_EVENT = 'sui'
50
+
51
+ const EMPTY_STC = {medium: null, source: null, campaign: null}
52
+
53
+ const loadScript = async src =>
54
+ new Promise(function (resolve, reject) {
55
+ const script = document.createElement('script')
56
+
57
+ script.src = src
58
+ script.onload = resolve
59
+ script.onerror = reject
60
+ document.head.appendChild(script)
61
+ })
62
+
63
+ export const loadGoogleAnalytics = async () => {
64
+ const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
65
+ const dataLayerName = getConfig('googleAnalyticsDataLayer') || DEFAULT_DATA_LAYER_NAME
66
+
67
+ // Check we have the needed config to load the script
68
+ if (!googleAnalyticsMeasurementId) return Promise.resolve(false)
69
+ // Create the `gtag` script
70
+ const gtagScript = `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsMeasurementId}&l=${dataLayerName}`
71
+ // Load it and retrieve the `clientId` from Google
72
+ return loadScript(gtagScript)
73
+ }
74
+
75
+ // Trigger GA init event just once per session.
76
+ // Only in LEGACY mode (manual GA4 initialization)
77
+ const triggerGoogleAnalyticsInitEvent = sessionId => {
78
+ // Don't trigger if using Segment destination (new mode)
79
+ if (isGA4DestinationEnabled()) return
80
+
81
+ const eventName = getConfig('googleAnalyticsInitEvent') ?? DEFAULT_GA_INIT_EVENT
82
+ const eventPrefix = `ga_event_${eventName}_`
83
+ const eventKey = `${eventPrefix}${sessionId}`
84
+
85
+ if (typeof window.gtag === 'undefined') return
86
+
87
+ // Check if the event has already been sent in this session.
88
+ if (!localStorage.getItem(eventKey)) {
89
+ // If not, send it.
90
+ window.gtag('event', eventName)
91
+
92
+ // eslint-disable-next-line no-console
93
+ console.log(`Sending GA4 event "${eventName}" for the session "${sessionId}"`)
94
+
95
+ // And then save a new GA session hit in local storage.
96
+ localStorage.setItem(eventKey, 'true')
97
+ dispatchEvent({eventName: EVENTS.GA4_INIT_EVENT_SENT, detail: {eventName, sessionId}})
98
+ }
99
+
100
+ // Clean old GA sessions hits from the storage.
101
+ Object.keys(localStorage).forEach(key => {
102
+ if (key.startsWith(eventPrefix) && key !== eventKey) {
103
+ localStorage.removeItem(key)
104
+ }
105
+ })
106
+ }
107
+
108
+ const getGoogleField = async field => {
109
+ const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
110
+
111
+ // If `googleAnalyticsMeasurementId` is not present, don't load anything.
112
+ if (!googleAnalyticsMeasurementId) return Promise.resolve()
113
+
114
+ return new Promise(resolve => {
115
+ // If it is, get it from `gtag`.
116
+ window.gtag?.('get', googleAnalyticsMeasurementId, field, resolve)
117
+ })
118
+ }
119
+
120
+ export const trackingTagsTypes = {
121
+ STC: 'stc',
122
+ UTM: 'utm'
123
+ }
124
+
125
+ export const getCampaignDetails = ({needsTransformation = true} = {}) => {
126
+ const {medium, source, campaign, content, term} = readQueryParams()
127
+
128
+ if (!medium || !source || !campaign) return null
129
+
130
+ const [id, name] = campaign.split(STC.CAMPAIGN_SPLIT_SYMBOL)
131
+
132
+ if (!id && !name) return null
133
+
134
+ const needsContent = typeof content !== 'undefined' && content !== STC_INVALID_CONTENT
135
+
136
+ return {
137
+ campaign: {
138
+ medium: (needsTransformation && STC_MEDIUM_TRANSFORMATIONS[medium]) || medium,
139
+ ...(typeof name !== 'undefined' && {id}),
140
+ name: name ?? id,
141
+ source,
142
+ ...(needsContent && {content}),
143
+ ...(typeof term !== 'undefined' && {term})
144
+ }
145
+ }
146
+ }
147
+
148
+ function readQueryParams() {
149
+ const search = utils.getActualQueryString()
150
+ const searchParams = new URLSearchParams(search)
151
+
152
+ return getConfig('trackingTagsType') === trackingTagsTypes.UTM ? readFromUtm(searchParams) : readFromStc(searchParams)
153
+ }
154
+
155
+ function readFromStc(searchParams) {
156
+ if (!hasStc(searchParams)) return EMPTY_STC
157
+
158
+ const stc = searchParams.get(STC.QUERY)
159
+ const [medium, source, campaign, content, term] = stc.split(STC.SPLIT_SYMBOL)
160
+ return {medium, source, campaign, content, term}
161
+ }
162
+
163
+ function hasStc(searchParams) {
164
+ return searchParams.has(STC.QUERY)
165
+ }
166
+
167
+ function readFromUtm(searchParams) {
168
+ const campaignId = searchParams.get(UTM_CAMPAIGN_ID)
169
+ const campaignName = searchParams.get(UTM_CAMPAIGN)
170
+ const medium = searchParams.get(UTM_MEDIUM)
171
+ const source = searchParams.get(UTM_SOURCE)
172
+ const content = searchParams.get(UTM_CONTENT)
173
+ const term = searchParams.get(UTM_TERM)
174
+ const campaign = campaignId ? `${campaignId}${STC.CAMPAIGN_SPLIT_SYMBOL}${campaignName}` : campaignName
175
+
176
+ // Fallback to STC if any mandatory UTM fields are missing
177
+ const missingAnyMandatoryFields = !medium || !source || !campaign
178
+ if (missingAnyMandatoryFields && hasStc(searchParams)) {
179
+ return readFromStc(searchParams)
180
+ }
181
+
182
+ return {
183
+ campaign,
184
+ medium,
185
+ source,
186
+ content,
187
+ term
188
+ }
189
+ }
190
+
191
+ export const getGoogleClientId = async () => getGoogleField(FIELDS.clientId)
192
+ export const getGoogleSessionId = async () => {
193
+ const sessionId = await getGoogleField(FIELDS.sessionId)
194
+
195
+ triggerGoogleAnalyticsInitEvent(sessionId)
196
+
197
+ return sessionId
198
+ }
199
+
200
+ // Unified consent state getter.
201
+ // Returns GRANTED, DENIED or undefined (default / unknown / unavailable).
202
+ export function getGoogleConsentValue(consentType = 'analytics_storage') {
203
+ try {
204
+ const value = window.google_tag_data?.ics?.getConsentState?.(consentType)
205
+
206
+ if (value === CONSENT_STATE_GRANTED_VALUE) return CONSENT_STATES.granted
207
+ if (value === CONSENT_STATE_DENIED_VALUE) return CONSENT_STATES.denied
208
+
209
+ return undefined
210
+ } catch (error) {
211
+ // GTM bug when first rejection happens: keep default undefined instead of forcing DENIED
212
+ return undefined
213
+ }
214
+ }
215
+
216
+ // Backwards compatibility alias (previous getConsentState behavior now unified but default is undefined)
217
+ export const getConsentState = () => getGoogleConsentValue() ?? CONSENT_STATES.denied
218
+
219
+ export const setGoogleUserId = userId => {
220
+ const googleAnalyticsMeasurementId = getConfig('googleAnalyticsMeasurementId')
221
+
222
+ if (!googleAnalyticsMeasurementId || !userId) return
223
+
224
+ window.gtag?.('set', 'user_id', userId)
225
+ }
226
+
227
+ /**
228
+ * Send consents to Google Consent Mode.
229
+ *
230
+ * @param {'default' | 'update'} mode Mode for the consent update
231
+ * @param {object} consents Consents object to be sent to Google Consent Mode.
232
+ * Defaults used when not provided:
233
+ * {
234
+ * analytics_storage: 'denied',
235
+ * ad_user_data: 'denied',
236
+ * ad_personalization: 'denied',
237
+ * ad_storage: 'denied'
238
+ * }
239
+ */
240
+ export const sendGoogleConsents = (mode = 'default', consents) => {
241
+ window.gtag?.(
242
+ 'consent',
243
+ mode,
244
+ consents || {
245
+ analytics_storage: CONSENT_STATES.denied,
246
+ ad_user_data: CONSENT_STATES.denied,
247
+ ad_personalization: CONSENT_STATES.denied,
248
+ ad_storage: CONSENT_STATES.denied
249
+ }
250
+ )
251
+ }
@@ -0,0 +1,48 @@
1
+ import {readCookie, removeCookie, saveCookie} from '../utils/cookies.js'
2
+ import {isClient, setConfig} from '../config.js'
3
+ import {USER_GDPR} from '../tcf.js'
4
+
5
+ const XANDR_ID_SERVER_URL = 'https://secure.adnxs.com/getuidj?consent=1'
6
+ const XANDR_ID_COOKIE = 'adit-xandr-id'
7
+
8
+ const USER_OPTED_OUT_XANDR_ID_VALUE = 0
9
+
10
+ /**
11
+ * [Xandr API Docs]{@link https://docs.xandr.com/bundle/invest_invest-standard/page/topics/user-id-mapping-with-getuid-and-mapuid.html}
12
+ * @returns {String|null} a valid xandrId value
13
+ */
14
+ export function getXandrId({gdprPrivacyValueAdvertising}) {
15
+ if (!isClient) return null
16
+
17
+ if (gdprPrivacyValueAdvertising !== USER_GDPR.ACCEPTED) {
18
+ removeCookie(XANDR_ID_COOKIE)
19
+ return null
20
+ }
21
+
22
+ // 0 is invalid. Negative numbers seems to be invalid too.
23
+ const checkValid = xandrId => xandrId && Number(xandrId) > USER_OPTED_OUT_XANDR_ID_VALUE
24
+
25
+ const storedXandrId = readCookie(XANDR_ID_COOKIE)
26
+ const isValidXandrId = checkValid(storedXandrId)
27
+
28
+ if (!isValidXandrId) {
29
+ getRemoteXandrId().then(xandrId => {
30
+ if (typeof xandrId === 'string') {
31
+ saveCookie(XANDR_ID_COOKIE, xandrId)
32
+ }
33
+ })
34
+ }
35
+ setConfig('xandrId', storedXandrId)
36
+ return isValidXandrId ? storedXandrId : null
37
+ }
38
+
39
+ function getRemoteXandrId() {
40
+ if (!isClient) return Promise.resolve(null)
41
+
42
+ return window
43
+ .fetch(XANDR_ID_SERVER_URL, {
44
+ credentials: 'include'
45
+ })
46
+ .then(response => response.json())
47
+ .then(json => json?.uid)
48
+ }