Skip to content

Commit

Permalink
[Consent Managment] Send consent changed event (#936)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Aug 22, 2023
1 parent b1584fc commit a7a0882
Show file tree
Hide file tree
Showing 26 changed files with 586 additions and 147 deletions.
6 changes: 6 additions & 0 deletions .changeset/mean-geese-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-consent-tools': minor
'@segment/analytics-consent-wrapper-onetrust': minor
---

Add consent changed event
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@internal/consent-tools-integration-tests",
"private": true,
"scripts": {
".": "yarn -T turbo run --filter=@internal/consent-tools-integration-tests",
".": "yarn run -T turbo run --filter=@internal/consent-tools-integration-tests...",
"dev": "yarn concurrently 'yarn watch' 'yarn build serve --open'",
"build": "webpack",
"watch": "yarn build --watch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { oneTrust } from '@segment/analytics-consent-wrapper-onetrust'
export const analytics = new AnalyticsBrowser()

oneTrust(analytics, {
disableConsentChangedEvent: false,
integrationCategoryMappings: {
Fullstory: ['C0001'],
'Actions Amplitude': ['C0004'],
Expand Down
31 changes: 19 additions & 12 deletions packages/consent/consent-tools/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
# @segment/analytics-consent-tools



## Quick Start

```ts
// wrapper.js
import { createWrapper, resolveWhen } from '@segment/analytics-consent-tools'

export const withCMP = createWrapper({
shouldLoad: (ctx) => {
await resolveWhen(() =>
window.CMP !== undefined && !window.CMP.popUpVisible(), 500)
await resolveWhen(
() => window.CMP !== undefined && !window.CMP.popUpVisible(),
500
)

if (noConsentNeeded) {
ctx.abort({ loadSegmentNormally: true })
} else if (allTrackingDisabled) {
ctx.abort({ loadSegmentNormally: false })
}
},
getCategories: () => {
getCategories: () => {
// e.g. { Advertising: true, Functional: false }
return normalizeCategories(window.CMP.consentedCategories())
}
return normalizeCategories(window.CMP.consentedCategories())
},
})
```


## Wrapper Usage API

## `npm`

```js
import { withCMP } from './wrapper'
import { AnalyticsBrowser } from '@segment/analytics-next'

export const analytics = new AnalyticsBrowser()

withCmp(analytics)
withCMP(analytics)

analytics.load({
writeKey: '<MY_WRITE_KEY'>
Expand All @@ -43,36 +45,41 @@ analytics.load({
```

## Snippet users (window.analytics)

1. Delete the `analytics.load()` line from the snippet

```diff
- analytics.load("<MY_WRITE_KEY>");
```

2. Import Analytics

```js
import { withCMP } from './wrapper'

withCmp(window.analytics)
withCMP(window.analytics)

window.analytics.load('<MY_WRITE_KEY')
```

## Wrapper Examples

- [OneTrust](../consent-wrapper-onetrust) (beta)

## Settings / Options / Configuration

See the complete list of settings in the **[Settings interface](src/types/settings.ts)**

## Development

1. Build this package + all dependencies

```sh
yarn . build
```

2. Run tests

```
yarn test
```


Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as ConsentStamping from '../consent-stamping'
import * as ConsentChanged from '../consent-changed'
import { createWrapper } from '../create-wrapper'
import { AbortLoadError, LoadContext } from '../load-cancellation'
import type {
CreateWrapperSettings,
AnyAnalytics,
CDNSettings,
AnalyticsBrowserSettings,
Categories,
} from '../../types'
import { CDNSettingsBuilder } from '@internal/test-helpers'
import { assertIntegrationsContainOnly } from './assertions/integrations-assertions'
Expand All @@ -29,6 +31,7 @@ const mockGetCategories: jest.MockedFn<CreateWrapperSettings['getCategories']> =
const analyticsLoadSpy: jest.MockedFn<AnyAnalytics['load']> = jest.fn()
const addSourceMiddlewareSpy = jest.fn()
let analyticsOnSpy: jest.MockedFn<AnyAnalytics['on']>
const analyticsTrackSpy: jest.MockedFn<AnyAnalytics['track']> = jest.fn()
let consoleErrorSpy: jest.SpiedFunction<typeof console['error']>

const getAnalyticsLoadLastCall = () => {
Expand Down Expand Up @@ -61,6 +64,7 @@ beforeEach(() => {
})

class MockAnalytics implements AnyAnalytics {
track = analyticsTrackSpy
on = analyticsOnSpy
load = analyticsLoadSpy
addSourceMiddleware = addSourceMiddlewareSpy
Expand Down Expand Up @@ -300,8 +304,16 @@ describe(createWrapper, () => {
)
})

describe('Validation', () => {
it('should throw an error if categories are in the wrong format', async () => {
describe('Settings Validation', () => {
/* NOTE: This test suite is meant to be minimal -- please see validation/__tests__ */

test('createWrapper should throw if user-defined settings/configuration/options are invalid', () => {
expect(() =>
wrapTestAnalytics({ getCategories: {} as any })
).toThrowError(/validation/i)
})

test('analytics.load should reject if categories are in the wrong format', async () => {
wrapTestAnalytics({
shouldLoad: () => Promise.resolve('sup' as any),
})
Expand All @@ -310,7 +322,7 @@ describe(createWrapper, () => {
)
})

it('should throw an error if categories are undefined', async () => {
test('analytics.load should reject if categories are undefined', async () => {
wrapTestAnalytics({
getCategories: () => undefined as any,
shouldLoad: () => undefined,
Expand Down Expand Up @@ -736,4 +748,60 @@ describe(createWrapper, () => {
})
})
})

describe('registerOnConsentChanged', () => {
const sendConsentChangedEventSpy = jest.spyOn(
ConsentChanged,
'sendConsentChangedEvent'
)

let categoriesChangedCb: (categories: Categories) => void = () => {
throw new Error('Not implemented')
}

const registerOnConsentChanged = jest.fn(
(consentChangedCb: (c: Categories) => void) => {
// simulate a OneTrust.onConsentChanged event callback
categoriesChangedCb = jest.fn((categories: Categories) =>
consentChangedCb(categories)
)
}
)
it('should expect a callback', async () => {
wrapTestAnalytics({
registerOnConsentChanged: registerOnConsentChanged,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)

expect(sendConsentChangedEventSpy).not.toBeCalled()
expect(registerOnConsentChanged).toBeCalledTimes(1)
categoriesChangedCb({ C0001: true, C0002: false })
expect(registerOnConsentChanged).toBeCalledTimes(1)
expect(sendConsentChangedEventSpy).toBeCalledTimes(1)

// if OnConsentChanged callback is called with categories, it should send event
expect(analyticsTrackSpy).toBeCalledWith(
'Segment Consent Preference',
undefined,
{ consent: { categoryPreferences: { C0001: true, C0002: false } } }
)
})
it('should throw an error if categories are invalid', async () => {
consoleErrorSpy.mockImplementationOnce(() => undefined)

wrapTestAnalytics({
registerOnConsentChanged: registerOnConsentChanged,
})

await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(consoleErrorSpy).not.toBeCalled()
categoriesChangedCb(['OOPS'] as any)
expect(consoleErrorSpy).toBeCalledTimes(1)
const err = consoleErrorSpy.mock.lastCall[0]
expect(err.toString()).toMatch(/validation/i)
// if OnConsentChanged callback is called with categories, it should send event
expect(sendConsentChangedEventSpy).not.toBeCalled()
expect(analyticsTrackSpy).not.toBeCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import type {
AnalyticsSnippet,
AnalyticsBrowser,
} from '@segment/analytics-next'
import { createWrapper } from '../../index'
import { createWrapper, AnyAnalytics } from '../../index'

type Extends<T, U> = T extends U ? true : false

{
const wrap = createWrapper({ getCategories: () => ({ foo: true }) })
wrap({} as AnalyticsBrowser)
wrap({} as AnalyticsSnippet)

// see AnalyticsSnippet and AnalyticsBrowser extend AnyAnalytics
const f: Extends<AnalyticsSnippet, AnyAnalytics> = true
const g: Extends<AnalyticsBrowser, AnyAnalytics> = true
console.log(f, g)
}
36 changes: 36 additions & 0 deletions packages/consent/consent-tools/src/domain/consent-changed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AnyAnalytics, Categories } from '../types'

/**
* Dispatch an event that looks like:
* ```ts
* {
* "type": "track",
* "event": "Segment Consent Preference",
* "context": {
* "consent": {
* "categoryPreferences" : {
* "C0001": true,
* "C0002": false,
* }
* }
* ...
* ```
*/
export const sendConsentChangedEvent = (
analytics: AnyAnalytics,
categories: Categories
): void => {
analytics.track(
CONSENT_CHANGED_EVENT,
undefined,
createConsentChangedCtxDto(categories)
)
}

const CONSENT_CHANGED_EVENT = 'Segment Consent Preference'

const createConsentChangedCtxDto = (categories: Categories) => ({
consent: {
categoryPreferences: categories,
},
})
22 changes: 20 additions & 2 deletions packages/consent/consent-tools/src/domain/create-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import {
CreateWrapperSettings,
CDNSettings,
} from '../types'
import { validateCategories, validateOptions } from './validation'
import {
validateAnalyticsInstance,
validateCategories,
validateSettings,
} from './validation'
import { createConsentStampingMiddleware } from './consent-stamping'
import { pipe, pick, uniq } from '../utils'
import { AbortLoadError, LoadContext } from './load-cancellation'
import { ValidationError } from './validation/validation-error'
import { sendConsentChangedEvent } from './consent-changed'

export const createWrapper: CreateWrapper = (createWrapperOptions) => {
validateOptions(createWrapperOptions)
validateSettings(createWrapperOptions)

const {
shouldDisableSegment,
Expand All @@ -23,9 +28,11 @@ export const createWrapper: CreateWrapper = (createWrapperOptions) => {
integrationCategoryMappings,
shouldEnableIntegration,
pruneUnmappedCategories,
registerOnConsentChanged,
} = createWrapperOptions

return (analytics) => {
validateAnalyticsInstance(analytics)
const ogLoad = analytics.load

const loadWithConsent: AnyAnalytics['load'] = async (
Expand Down Expand Up @@ -118,6 +125,17 @@ export const createWrapper: CreateWrapper = (createWrapperOptions) => {
createConsentStampingMiddleware(getValidCategoriesForConsentStamping)
)

// whenever consent changes, dispatch a new event with the latest consent information
registerOnConsentChanged?.((categories) => {
try {
validateCategories(categories)
sendConsentChangedEvent(analytics, categories)
} catch (err) {
// Not sure if there's a better way to handle this, but this makes testing a bit easier.
console.error(err)
}
})

const updateCDNSettings: InitOptions['updateCDNSettings'] = (
cdnSettings
) => {
Expand Down
Loading

0 comments on commit a7a0882

Please sign in to comment.