Skip to content

Commit

Permalink
Add consent opt out (#1063)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Apr 17, 2024
1 parent f898a8c commit 8f7a74a
Show file tree
Hide file tree
Showing 51 changed files with 1,717 additions and 664 deletions.
6 changes: 6 additions & 0 deletions .changeset/tall-hornets-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-consent-tools': major
'@segment/analytics-consent-wrapper-onetrust': major
---

Add opt-out consent-model support, and use opt-out by default
9 changes: 9 additions & 0 deletions packages/consent/consent-tools-integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ Why is this using wd.io instead of playwright?
```
yarn . test:int
```

### Debugging Tips:
- Webdriver.io has the handy `browser.debug()` command.

- You can serve the static pages by themselves (without webdriver.io) with the following:
```
yarn webpack -w &
npx live-server .
```
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
<!DOCTYPE html>
<html lang="en">

<head>
<script src="dist/consent-tools-vanilla.bundle.js"></script>
</head>

<body>
<h1>Hello World - Serving Analytics</h1>
<h2>Please Check Network tab</h2>
<p>This page can used as playground or run by webdriver.io</p>
<script src="/public/dist/consent-tools-vanilla-opt-in.bundle.js"></script>
</body>

</html>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">

<body>
<h1>Hello World - Serving Analytics (Consent Tools Vanilla Opt Out)</h1>
<h2>Please Check Network tab</h2>
<p>This page can used as playground or run by webdriver.io</p>
<script src="/public/dist/consent-tools-vanilla-opt-out.bundle.js"></script>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<script src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js" type="text/javascript" charset="UTF-8"
data-domain-script="80ca7b5c-e72f-4bd0-972a-b74d052a0820-test"></script>

<script src="/@segment/analytics-consent-wrapper-onetrust/dist/umd/analytics-onetrust.umd.js"></script>
<script src="/node_modules/@segment/analytics-consent-wrapper-onetrust/dist/umd/analytics-onetrust.umd.js"></script>

<script>
!(function () {
Expand Down Expand Up @@ -55,7 +55,7 @@
var t = document.createElement('script')
t.type = 'text/javascript'
t.async = !0
t.src = '/@segment/analytics-next/dist/umd/standalone.js' // modified
t.src = '/node_modules/@segment/analytics-next/dist/umd/standalone.js' // modified
var n = document.getElementsByTagName('script')[0]
n.parentNode.insertBefore(t, n)
analytics._loadOptions = e
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AnalyticsBrowser } from '@segment/analytics-next'
import { initMockConsentManager } from '../helpers/mock-cmp'
import { withMockCMP } from '../helpers/mock-cmp-wrapper'

initMockConsentManager({ consentModel: 'opt-in' })

const analytics = new AnalyticsBrowser()

// for testing
;(window as any).analytics = analytics

withMockCMP(analytics).load({
writeKey: 'foo',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AnalyticsBrowser } from '@segment/analytics-next'
import { initMockConsentManager } from '../helpers/mock-cmp'
import { withMockCMP } from '../helpers/mock-cmp-wrapper'

initMockConsentManager({
consentModel: 'opt-out',
})

const analytics = new AnalyticsBrowser()

withMockCMP(analytics).load(
{
writeKey: '9lSrez3BlfLAJ7NOChrqWtILiATiycoc',
},
{ initialPageview: true }
)
;(window as any).analytics = analytics

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createWrapper, resolveWhen } from '@segment/analytics-consent-tools'

export const withMockCMP = (analyticsInstance: any) =>
createWrapper({
enableDebugLogging: true,
shouldLoadWrapper: async () => {
await resolveWhen(() => window.MockCMP.isLoaded, 500)
},
getCategories: () => window.MockCMP.getCategories(),
registerOnConsentChanged: (fn) => {
window.MockCMP.onConsentChange(fn)
},
shouldLoadSegment: async (ctx) => {
if (window.MockCMP.consentModel === 'opt-out') {
// if opt out, load immediately
return ctx.load({ consentModel: 'opt-out' })
}
await window.MockCMP.waitForAlertBoxClose()
ctx.load({ consentModel: 'opt-in' })
},
})(analyticsInstance)
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const constants = {
giveConsentId: 'give-consent',
denyConsentId: 'deny-consent',
}
export type MockConsentManager = ReturnType<typeof initMockConsentManager>

declare global {
interface Window {
MockCMP: MockConsentManager
}
}
type ConsentChangeFn = (categories: Record<string, boolean>) => void

/**
* This is a mock consent manager that simulates a consent manager that is loaded asynchronously.
* Similar to OneTrust, TrustArc, etc.
* sets a global `window.MockCMP` object that can be used to interact with the mock consent manager.
*/
export const initMockConsentManager = (settings: { consentModel: string }) => {
const isOptIn = settings.consentModel === 'opt-in'
// if opt-in is true, all categories are set to true by default
let categories = {
FooCategory1: isOptIn,
FooCategory2: isOptIn,
}
console.log('initMockConsentManager', settings, categories)

let onConsentChange = (_categories: Record<string, boolean>) =>
undefined as void

const createAlertBox = () => {
const container = document.createElement('div')
container.id = 'alert-box'
container.innerHTML = `
<button id="${constants.giveConsentId}">Give consent</button>
<button id="${constants.denyConsentId}">Deny consent</button>
`
return container
}

const alertBox = createAlertBox()
let loaded = false
setTimeout(() => {
loaded = true
mockCMPPublicAPI.openAlertBox()
}, 300)

/**
* similar to window.OneTrust
*/
const mockCMPPublicAPI = {
get isLoaded() {
return loaded
},
get consentModel() {
return settings.consentModel
},
setCategories: (newCategories: Record<string, boolean>) => {
categories = { ...categories, ...newCategories }
onConsentChange(categories)
return categories
},
waitForAlertBoxClose() {
return new Promise<void>((resolve) => {
document
.getElementById('give-consent')!
.addEventListener('click', () => {
resolve()
})

document
.getElementById('deny-consent')!
.addEventListener('click', () => {
resolve()
})
})
},
getCategories: () => categories,
openAlertBox: () => {
document.body.appendChild(alertBox)
document
.getElementById(constants.giveConsentId)!
.addEventListener('click', () => {
mockCMPPublicAPI.setCategories({
FooCategory1: true,
FooCategory2: true,
})
mockCMPPublicAPI.closeAlertBox()
})
document
.getElementById(constants.denyConsentId)!
.addEventListener('click', () => {
mockCMPPublicAPI.setCategories({
FooCategory1: false,
FooCategory2: false,
})
mockCMPPublicAPI.closeAlertBox()
})
},
closeAlertBox: () => {
document.body.removeChild(alertBox)
},
onConsentChange: (fn: ConsentChangeFn) => {
onConsentChange = fn
},
}
window.MockCMP = mockCMPPublicAPI
return mockCMPPublicAPI
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CDNSettingsBuilder } from '@internal/test-helpers'
import type { SegmentEvent } from '@segment/analytics-next'
import assert from 'assert'
import type { Matches } from 'webdriverio'

const waitUntilReady = () =>
browser.waitUntil(
Expand All @@ -12,34 +14,90 @@ const waitUntilReady = () =>
export abstract class BasePage {
constructor(protected page: string) {}

segmentTrackingApiReqs: Matches[] = []
fetchIntegrationReqs: Matches[] = []

async load(): Promise<void> {
const baseURL = browser.options.baseUrl
assert(baseURL)
await this.mockCDNSettingsEndpoint()
await this.mockAPIs()
await browser.url(baseURL + '/public/' + this.page)
await waitUntilReady()
await browser.url(baseURL + '/' + this.page)
}

async clearStorage() {
await browser.deleteAllCookies()
await browser.execute(() => localStorage.clear())
await browser.execute(() => window.localStorage.clear())
}

/**
* Mock the CDN Settings endpoint so that this can run offline
getAllTrackingEvents(): SegmentEvent[] {
const reqBodies = this.segmentTrackingApiReqs.map((el) =>
JSON.parse(el.postData!)
)
return reqBodies
}

getConsentChangedEvents(): SegmentEvent[] {
const reqBodies = this.getAllTrackingEvents()
const consentEvents = reqBodies.filter(
(el) => el.event === 'Segment Consent Preference'
)
return consentEvents
}

async cleanup() {
this.segmentTrackingApiReqs = []
this.fetchIntegrationReqs = []
await this.clearStorage()
}

async mockAPIs() {
await this.mockSegmentTrackingAPI()
await this.mockCDNSettingsAPI()
await this.mockNextIntegrationsAPI()
}

private async mockSegmentTrackingAPI(): Promise<void> {
const mock = await browser.mock('https://api.segment.io/v1/t', {
method: 'post',
})
mock.respond((mock) => {
this.segmentTrackingApiReqs.push(mock)
// response with status 200
return Promise.resolve({
statusCode: 200,
body: JSON.stringify({ success: true }),
})
})
}

private async mockNextIntegrationsAPI(): Promise<void> {
const mock = await browser.mock('**/next-integrations/**')
mock.respond((mock) => {
this.fetchIntegrationReqs.push(mock)
return Promise.resolve({
statusCode: 200,
body: 'console.log("mocking action and classic destinations")',
})
})
}

/** * Mock the CDN Settings endpoint so that this can run offline
*/
private mockCDNSettingsEndpoint(): Promise<void> {
private async mockCDNSettingsAPI(): Promise<void> {
const settings = new CDNSettingsBuilder({
writeKey: 'something',
})
.addActionDestinationSettings(
{
url: 'https://cdn.segment.com/next-integrations/actions/fullstory-plugins/foo.js',
creationName: 'FullStory',
consentSettings: {
categories: ['FooCategory2'],
},
},
{
url: 'https://cdn.segment.com/next-integrations/actions/amplitude-plugins/foo.js',
creationName: 'Actions Amplitude',
consentSettings: {
categories: ['FooCategory1'],
Expand All @@ -48,17 +106,13 @@ export abstract class BasePage {
)
.build()

return browser
.mock('**/settings')
.then((mock) =>
mock.respond(settings, {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
})
)
.catch(console.error)
const mock = await browser.mock('**/settings')
mock.respond(settings, {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
})
}

/**
Expand Down
Loading

0 comments on commit 8f7a74a

Please sign in to comment.