Skip to content

Commit 64e4be8

Browse files
authored
feat: check user opt-out when blocking trackers
Adds check for user manual tracking opt out to tracking module
1 parent 3f394f5 commit 64e4be8

File tree

5 files changed

+127
-2
lines changed

5 files changed

+127
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"prettier": "prettier --ignore-path .prettierignore \"./**/*.{js,ts,tsx,json}\"",
1818
"format": "yarn lint:fix && yarn prettier --write",
1919
"format:verify": "yarn prettier --check",
20+
"compile": "yarn verify",
2021
"verify": "turbo run verify --concurrency=3",
2122
"verify-all": "yarn verify",
2223
"clear-modules": "lerna clean -y && rm -rf node_modules",
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Consent } from '../consent';
2+
import {
3+
getConsentDecision,
4+
OPT_OUT_DATALAYER_VAR,
5+
} from '../getConsentDecision';
6+
import { TrackingWindow } from '../types';
7+
8+
const MINIMUM_CONSENT = [Consent.StrictlyNecessary];
9+
10+
const FULL_CONSENT = [
11+
Consent.StrictlyNecessary,
12+
Consent.Functional,
13+
Consent.Performance,
14+
Consent.Targeting,
15+
];
16+
17+
const FULL_CONSENT_STRING = [',', ...FULL_CONSENT].join(',');
18+
19+
describe('getConsentDecision', () => {
20+
it('converts a stringified consent decision into an array', () => {
21+
const result = getConsentDecision({
22+
scope: {
23+
OnetrustActiveGroups: FULL_CONSENT_STRING,
24+
},
25+
});
26+
expect(result).toEqual(FULL_CONSENT);
27+
});
28+
29+
it('does not modify an array formatted consent decision', () => {
30+
const result = getConsentDecision({
31+
scope: {
32+
OnetrustActiveGroups: FULL_CONSENT,
33+
},
34+
});
35+
expect(result).toEqual(FULL_CONSENT);
36+
});
37+
38+
describe('optedOutExternalTracking', () => {
39+
it('reduces the consent decision to necessary and functional for opted out users', () => {
40+
const result = getConsentDecision({
41+
scope: {
42+
OnetrustActiveGroups: FULL_CONSENT,
43+
},
44+
optedOutExternalTracking: true,
45+
});
46+
expect(result).toEqual([Consent.StrictlyNecessary, Consent.Functional]);
47+
});
48+
49+
it('does not add Functional tracking if the user has opted out of it', () => {
50+
const result = getConsentDecision({
51+
scope: {
52+
OnetrustActiveGroups: MINIMUM_CONSENT,
53+
},
54+
optedOutExternalTracking: true,
55+
});
56+
expect(result).toEqual(MINIMUM_CONSENT);
57+
});
58+
59+
it('triggers the opt out datalayer variable', () => {
60+
const scope: TrackingWindow = {
61+
OnetrustActiveGroups: FULL_CONSENT,
62+
};
63+
getConsentDecision({
64+
scope,
65+
optedOutExternalTracking: true,
66+
});
67+
const dataLayerVars = scope.dataLayer
68+
?.map((v: Record<string, unknown>) => Object.keys(v))
69+
.flat();
70+
expect(dataLayerVars).toEqual([OPT_OUT_DATALAYER_VAR]);
71+
});
72+
});
73+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Consent } from './consent';
2+
import { TrackingWindow } from './types';
3+
4+
export interface ConsentDecisionOptions {
5+
scope: TrackingWindow;
6+
optedOutExternalTracking?: boolean;
7+
}
8+
9+
export const OPT_OUT_DATALAYER_VAR = 'user_opted_out_external_tracking';
10+
11+
export const getConsentDecision = ({
12+
scope,
13+
optedOutExternalTracking,
14+
}: ConsentDecisionOptions) => {
15+
let consentDecision: Consent[] = [];
16+
17+
if (typeof scope.OnetrustActiveGroups === 'string') {
18+
consentDecision = scope.OnetrustActiveGroups.split(',').filter(
19+
Boolean
20+
) as Consent[];
21+
} else if (scope.OnetrustActiveGroups) {
22+
consentDecision = scope.OnetrustActiveGroups;
23+
}
24+
25+
if (optedOutExternalTracking) {
26+
/**
27+
* If user has already opted out of everything but the essentials
28+
* don't force them to consent to Functional trackers
29+
*/
30+
if (consentDecision.length > 1) {
31+
consentDecision = [Consent.StrictlyNecessary, Consent.Functional];
32+
}
33+
scope.dataLayer ??= [];
34+
scope.dataLayer.push({ [OPT_OUT_DATALAYER_VAR]: true });
35+
}
36+
37+
return consentDecision;
38+
};

packages/tracking/src/integrations/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { conditionallyLoadAnalytics } from './conditionallyLoadAnalytics';
22
import { fetchDestinationsForWriteKey } from './fetchDestinationsForWriteKey';
3+
import { getConsentDecision } from './getConsentDecision';
34
import { mapDestinations } from './mapDestinations';
45
import { initializeOneTrust } from './onetrust';
56
import { runSegmentSnippet } from './runSegmentSnippet';
@@ -26,6 +27,11 @@ export type TrackingIntegrationsSettings = {
2627
*/
2728
user?: UserIntegrationSummary;
2829

30+
/**
31+
* Whether user has opted out or is excluded from external tracking
32+
*/
33+
optedOutExternalTracking?: boolean;
34+
2935
/**
3036
* Segment write key.
3137
*/
@@ -40,6 +46,7 @@ export const initializeTrackingIntegrations = async ({
4046
production,
4147
scope,
4248
user,
49+
optedOutExternalTracking,
4350
writeKey,
4451
}: TrackingIntegrationsSettings) => {
4552
// 1. Wait 1000ms to allow any other post-hydration logic to run first
@@ -56,13 +63,19 @@ export const initializeTrackingIntegrations = async ({
5663
onError,
5764
writeKey,
5865
});
66+
5967
if (!destinations) {
6068
return;
6169
}
6270

71+
const consentDecision = getConsentDecision({
72+
scope,
73+
optedOutExternalTracking,
74+
});
75+
6376
// 5. Those integrations are compared against the user's consent decisions into a list of allowed destinations
6477
const { destinationPreferences, identifyPreferences } = mapDestinations({
65-
consentDecision: scope.OnetrustActiveGroups,
78+
consentDecision,
6679
destinations,
6780
});
6881

packages/tracking/src/integrations/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ export interface SegmentAnalyticsOptions {
2828
export interface TrackingWindow {
2929
analytics?: SegmentAnalytics;
3030
dataLayer?: unknown[];
31-
OnetrustActiveGroups?: Consent[];
31+
OnetrustActiveGroups?: Consent[] | string;
3232
OptanonWrapper?: () => void;
3333
}

0 commit comments

Comments
 (0)