Skip to content

Commit

Permalink
feat(packages/sui-segment-wrapper): add segment wrapper package
Browse files Browse the repository at this point in the history
  • Loading branch information
kikoruiz committed Sep 18, 2024
1 parent 1b5bfd9 commit caca7a7
Show file tree
Hide file tree
Showing 36 changed files with 2,899 additions and 0 deletions.
251 changes: 251 additions & 0 deletions packages/sui-segment-wrapper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Segment Wrapper

This package adds an abstraction layer on top of [segment.com](https://segment.com/)'s [javascript library](https://segment.com/docs/sources/website/analytics.js/) with some useful integrations and fixes:

**Data Quality 📈**

- [x] Add `page` method that internally uses `track` but with the correct `referrer` property.
- [x] Send `user.id` and `anonymousId` on every track.
- [x] Send anonymous data to AWS to be able to check data integrity with Adobe.

**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`).
- [x] Fetch `marketingCloudVisitorId` and put in integrations object for every track.
- [x] Monkey patch `track` native Segment method to send `marketingCloudVisitorId` inside `context.integrations`.

**Consent Management Platform 🐾**

- [x] Automatic tracking of Consent Management Platform usage.
- [x] Send `gdpr_privacy` on `context` for for all tracking events. Expect values: `unknown`, `accepted` and `declined`.
- [x] Send `gdpr_privacy_advertising` on `context` for for all tracking events. Expect values: `unknown`, `accepted` and `declined`.
- [x] `gdpr_privacy` is accepted when consents [1, 8, 10] are accepted and `gdpr_privacy_advertising` is accepted when consents [3] is accepted.
- [x] Any track is sent if the user has not accepted/rejected any consent yet.

**Developer Experience 👩‍💻**

- [x] Always send `platform` property as `web`.
- [x] Allow to configure `window.__mpi.defaultContext` to send these properties on all tracks inside the context object.
- [x] Allow to configure `window.__mpi.defaultProperties` to send these properties on all tracks.

**Segment Middlewares 🖖**

- [x] Optimizely Full Stack middleware to use Segment's anonymousId as Optimizely's userId, more info [here](#optimizelys-userid)

## Usage

After adding your Segment snippet into your html, you'll need to include this package in order to be able to fire events.

`analytics` will be an object with the methods described [here](#events)

### Step 1: Copy the Snippet in your HTML

```html
<script>
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.src="https://cdn.segment.com/analytics.js/v1/" + key + "/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey="YOUR_WRITE_KEY";analytics.SNIPPET_VERSION="4.13.2";
analytics.load("YOUR_WRITE_KEY"); // your write key must be set here
}}();
</script>
```

### Step 2: Use

#### In your modern and beautiful JavaScript...

```js
import analytics from '@s-ui/segment-wrapper'
```

### In your monolithic JavaScript...

```js
// First load the UMD module.
<script src="https://unpkg.com/@s-ui/segment-wrapper/umd/index.js"></script>
<script>
// Then trigger all the events you need referencing the right namespaced
// object: `window.sui.analytics`. For more info see the "Events" section below.
window.sui.analytics.identify('your user id', {});
window.sui.analytics.track('your event', {});
window.sui.analytics.reset();
</script>
```

### Step 3: Configure mandatory Segment Wrapper attributes:

The following configuration parameters are required and must be set for the system to function correctly:
- `ADOBE_ORG_ID`: This parameter is the Adobe Organization ID, required for integration with Adobe services. Please make sure that you replace the example value with your actual Adobe Org ID.
- `TRACKING_SERVER`: This specifies the tracking server URL that will be used for sending data and handling tracking requests.

These parameters need to be defined in the `window._SEGMENT_WRAPPER` object as follows:

```js
window.__SEGMENT_WRAPPER = {
ADOBE_ORG_ID: '012345678@AdobeOrg', // Mandatory!
TRACKING_SERVER: 'mycompany.test.net', // Mandatory!
}
```

Configure both values correctly before running the application to ensure proper tracking and data integration.

### Step 4: Configure Segment Wrapper (optional)

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.

- `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`.
- `getCustomAdobeVisitorId`: _(optional)_ If set, the output of this function will be used as `marketingCloudVisitorId` in Adobe Analytics' integration. It must return a promise.
- `importAdobeVisitorId` _(optional)_ If set and `true`, Adobe Visitor API will be fetched from Segment Wrapper instead of relying on being loaded before from Tealium or other services. Right now, by default, this is `false` but in the next major this configuration will be `true` by default. If `getCustomAdobeVisitorId` is being used this will be ignored.
- `tcfTrackDefaultProperties` _(optional)_ If set, this property will be merged together with the default properties set to send with every tcf track event
- `universalId`: _(optional)_ If set this value will be used for the Visitor API and other services.
- `hashedUserEmail`: _(optional)_ If set and not `universalId` is set this value will be used for the Visitor API and other services.
- `userEmail`: _(optional)_ If set and not `universalId` is available, this value will be hashed with SHA-256 and used for the Visitor API and other services.
- `isUserTraitsEnabled`: _(optional)_ If set context traits will be populated with all user traits.

Example:

```js
window.__mpi = {
segmentWrapper: {
universalId: '7ab9ddf3281d5d5458a29e8b3ae2864',
defaultContext: {
site: 'comprocasa',
vertical: 'realestate'
},
getCustomAdobeVisitorId: () => {
const visitorId = // get your visitorId
return Promise.resolve(visitorId)
},
tcfTrackDefaultProperties: {
tcfSpecialProp: 'anyvalue'
}
}
}
```

### It also provides additional information such as:

- window.__mpi.isFirstVisit: boolean - true if the user hasn't interacted with the tcf modal yet

## Events

### Track - [docs](https://segment.com/docs/spec/track/)

```js
import analytics from '@s-ui/segment-wrapper'

analytics.track('CTA Clicked')

analytics.track('Registered', {
plan: 'Pro Annual',
accountType: 'Facebook'
})
```

### Page

Internally uses `Track` but changes the `referrer` everytime is called.

```js
import analytics from '@s-ui/segment-wrapper'

analytics.page('Home Viewed')

analytics.page('List Viewed', {
property: 'HOUSE',
transaction: 'SELL'
})
```

### Identify - [docs](https://segment.com/docs/spec/identify/)

```js
import analytics from '@s-ui/segment-wrapper'

analytics.identify('97980cfea0067', {
name: 'Peter Gibbons',
email: '[email protected]',
plan: 'premium',
logins: 5
})
```

### Reset - [docs](https://segment.com/docs/sources/website/analytics.js/#reset-logout)

```js
import analytics from '@s-ui/segment-wrapper'

analytics.reset()
```

## UniversalID

*Segment Wrapper* is handling all about the UniversalID, an ID to identify the user across different sites by using a hashed email. If you want, you could subscribe yourself to an event in order to retrieve this info:

```js
document.addEventListener(USER_DATA_READY_EVENT, e => {
const {universalId} = e.detail
})
```

Also, you could check directly if the `universalId` is already available on the window:

```js
const {universalId} = window.__mpi.segmentWrapper
```

Or use both systems:

```js
let {universalId, universalIdInitialized} = window.__mpi.segmentWrapper

if (!universalId && !universalIdInitialized) {
document.addEventListener(USER_DATA_READY_EVENT, e => {
universalId = e.detail.universalId
doSomethingWithUniversalId(universalId)
})
}

console.log(universalId)
```

### Send xandrId as externalIds

To not send the ```xandrId``` put this flag as configuration: ```window.__mpi.segmentWrapper.sendXandrId = false```

By default, all xandrId will be sent.


## Middlewares

You can find info about segment's middleware [here](https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/middleware/)

### Optimizely's userId

Will use segment's anonymousId as optimizely's userId

#### How to

```js
import {optimizelyUserId} from '@s-ui/segment-wrapper/lib/middlewares/source/optimizelyUserId'

window.analytics.ready(() => {
window.analytics.addSourceMiddleware(optimizelyUserId)
})
```

### Optimizely's site attribute

Will add the site property as optimizely attribute

#### How to

```js
import {optimizelySiteAttributeMiddleware} from '@s-ui/segment-wrapper/lib/middlewares/destination/optimizelySiteAttribute'

window.analytics.ready(() => {
window.analytics.addDestinationMiddleware('Optimizely', [
optimizelySiteAttributeMiddleware
])
})
```
26 changes: 26 additions & 0 deletions packages/sui-segment-wrapper/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@s-ui/segment-wrapper",
"version": "4.0.0",
"description": "Abstraction layer on top of the Segment library.",
"main": "lib/index.js",
"scripts": {
"lib": "sui-js-compiler",
"prepublishOnly": "npm run umd && npm run lib",
"test:client:watch": "npm run test:client -- --watch",
"test:client": "sui-test browser --src-pattern=src/index.js -H",
"test": "npm run test:client",
"test:umd": "npm run umd && npx servor ./umd",
"umd": "sui-bundler lib src-umd/index.js -o umd/ -p --root"
},
"author": "",
"license": "ISC",
"dependencies": {
"@s-ui/js": "2",
"tiny-hashes": "1.0.1"
},
"devDependencies": {
"@s-ui/bundler": "9",
"@s-ui/js-compiler": "1",
"@s-ui/test": "8"
}
}
9 changes: 9 additions & 0 deletions packages/sui-segment-wrapper/src-umd/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {getAdobeMCVisitorID, getAdobeVisitorData} from '../src/adobeRepository.js'
import analytics from '../src/index.js'

const w = window

w.sui = w.sui || {}
w.sui.analytics = w.sui.analytics || analytics
w.sui.analytics.getAdobeVisitorData = w.sui.analytics.getAdobeVisitorData || getAdobeVisitorData
w.sui.analytics.getAdobeMCVisitorID = w.sui.analytics.getAdobeMCVisitorID || getAdobeMCVisitorID
72 changes: 72 additions & 0 deletions packages/sui-segment-wrapper/src/adobeRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {getConfig} from './config.js'

let mcvid

const getGlobalConfig = () => {
return {
ADOBE_ORG_ID: window.__SEGMENT_WRAPPER?.ADOBE_ORG_ID,
DEFAULT_DEMDEX_VERSION: window.__SEGMENT_WRAPPER?.DEFAULT_DEMDEX_VERSION ?? '3.3.0',
TIME_BETWEEN_RETRIES: window.__SEGMENT_WRAPPER?.TIME_BETWEEN_RETRIES ?? 15,
TIMES_TO_RETRY: window.__SEGMENT_WRAPPER?.TIMES_TO_RETRY ?? 80,
SERVERS: {
trackingServer: window.__SEGMENT_WRAPPER?.TRACKING_SERVER,
trackingServerSecure: window.__SEGMENT_WRAPPER?.TRACKING_SERVER
}
}
}

const getDemdex = () => {
const config = getGlobalConfig()

return window.Visitor && window.Visitor.getInstance(config.ADOBE_ORG_ID, config.SERVERS)
}

const getMarketingCloudVisitorID = demdex => {
const mcvid = demdex && demdex.getMarketingCloudVisitorID()
return mcvid
}

const getAdobeVisitorData = () => {
const demdex = getDemdex() || {}
const config = getGlobalConfig()
const {version = config.DEFAULT_DEMDEX_VERSION} = demdex
const {trackingServer} = config.SERVERS

return Promise.resolve({trackingServer, version})
}

export const getAdobeMarketingCloudVisitorIdFromWindow = () => {
if (mcvid) return Promise.resolve(mcvid)

const config = getGlobalConfig()

return new Promise(resolve => {
function retry(retries) {
if (retries === 0) return resolve('')

const demdex = getDemdex()
mcvid = getMarketingCloudVisitorID(demdex)
return mcvid ? resolve(mcvid) : window.setTimeout(() => retry(--retries), config.TIME_BETWEEN_RETRIES)
}
retry(config.TIMES_TO_RETRY)
})
}

const importVisitorApiAndGetAdobeMCVisitorID = () =>
import('./adobeVisitorApi.js').then(() => {
mcvid = getAdobeMarketingCloudVisitorIdFromWindow()
return mcvid
})

const getAdobeMCVisitorID = () => {
const getCustomAdobeVisitorId = getConfig('getCustomAdobeVisitorId')
if (typeof getCustomAdobeVisitorId === 'function') {
return getCustomAdobeVisitorId()
}

return getConfig('importAdobeVisitorId') === true
? importVisitorApiAndGetAdobeMCVisitorID()
: getAdobeMarketingCloudVisitorIdFromWindow()
}

export {getAdobeVisitorData, getAdobeMCVisitorID}
12 changes: 12 additions & 0 deletions packages/sui-segment-wrapper/src/adobeVisitorApi.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions packages/sui-segment-wrapper/src/checkAnonymousId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const checkAnonymousId = () => {
const SEGMENT_ID_USER_WITHOUT_CONSENTS =
window.__SEGMENT_WRAPPER?.SEGMENT_ID_USER_WITHOUT_CONSENTS ?? 'anonymous_user'

const anonymousId = window.analytics.user?.()?.anonymousId()

if (anonymousId === SEGMENT_ID_USER_WITHOUT_CONSENTS) {
window.analytics.setAnonymousId(null)
}
}
24 changes: 24 additions & 0 deletions packages/sui-segment-wrapper/src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const MPI_CONFIG_KEY = '__mpi'

export const isClient = typeof window !== 'undefined'

/**
* Get the Segment Wrapper config from window
* @param {string=} key Key config to extract. If not provided, all the config will be returned
* @return {any} Config value or all the config if not key provided
*/
export const getConfig = key => {
const config = window?.[MPI_CONFIG_KEY]?.segmentWrapper || {}
return key ? config[key] : config
}

/**
* Set a config value to the Segment Wrapper config
* @param {string} key Config key to update
* @param {boolean|string|number|object} value Value to set on the config key
*/
export const setConfig = (key, value) => {
window[MPI_CONFIG_KEY] = window[MPI_CONFIG_KEY] || {}
window[MPI_CONFIG_KEY].segmentWrapper = window[MPI_CONFIG_KEY].segmentWrapper || {}
window[MPI_CONFIG_KEY].segmentWrapper[key] = value
}
Loading

0 comments on commit caca7a7

Please sign in to comment.